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

opendefensecloud / solution-arsenal / 30623048857

31 Jul 2026 10:17AM UTC coverage: 81.918% (-0.04%) from 81.954%
30623048857

push

github

web-flow
fix: restructured renderartifact and -binding to solve a known problem (#724)

## What
Changed the structure of RenderArtifacts and -bindings to reference
Registries instead of having a copy of the secret
Closes #616 

## Why
RenderArtifacts had possibly stale secrets with the old implementation,
and secrets would need to be repinned, dependent on remaining
ArtifatBindings etc., which was convoluted and not ideal for the simple
reason that we don't want the registry secrets or other sensitive data
copied in too many ressources.

## Testing
make test

## Notes for reviewers
RenderArtifact lost some Registry-Specific Fields, and RenderArtifact as
well as ArtifatBindings got a RegistryRef field.

## 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**
* Render artifacts now reference a Registry for credentials and
transport settings.
* Render bindings preserve Registry references and support automatic
artifact re-pinning.
* Cross-namespace Registry access is supported through explicit
authorization grants.
  * Registry changes are propagated to existing artifacts and bindings.

* **Bug Fixes**
* Improved OCI cleanup authentication, host validation, and
insecure-transport handling.
* Unauthorized cleanup now reports a clear authentication failure
status.

* **Documentation**
* Updated API references, user guidance, and configuration examples for
Registry references and access grants.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

92 of 102 new or added lines in 2 files covered. (90.2%)

7 existing lines in 3 files now uncovered.

5246 of 6404 relevant lines covered (81.92%)

29.47 hits per line

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

86.58
/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
        annotationJobName    = "solar.opendefense.cloud/job-name"
30
        annotationSecretName = "solar.opendefense.cloud/secret-name"
31

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

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

41
// RenderTaskReconciler reconciles a RenderTask object.
42
// Each RenderTask carries its own BaseURL and PushSecretRef for the target registry.
43
type RenderTaskReconciler struct {
44
        client.Client
45
        Scheme              *runtime.Scheme
46
        Recorder            events.EventRecorder
47
        RendererImage       string
48
        RendererCommand     string
49
        RendererArgs        []string
50
        RendererCAConfigMap string
51
        // RendererImagePullSecrets is the list of Secret names that kubelets in
52
        // each RenderTask namespace should use to pull the renderer image. Each
53
        // name must reference an existing Secret of type
54
        // kubernetes.io/dockerconfigjson in the RenderTask's namespace.
55
        RendererImagePullSecrets []string
56
        // WatchNamespace restricts reconciliation to this namespace.
57
        // Should be empty in production (watches all namespaces).
58
        // Intended for use in integration tests only.
59
        // See: https://book.kubebuilder.io/reference/envtest#testing-considerations
60
        WatchNamespace string
61
}
62

63
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
64
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks/status,verbs=get;update;patch
65
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks/finalizers,verbs=update
66
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
67
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
68
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
69

70
// Reconcile moves the current state of the cluster closer to the desired state
71
func (r *RenderTaskReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
453✔
72
        log := ctrl.LoggerFrom(ctx)
453✔
73
        ctrlResult := ctrl.Result{}
453✔
74

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

453✔
77
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
490✔
78
                return ctrlResult, nil
37✔
79
        }
37✔
80

81
        // Fetch the RenderTask instance
82
        res := &solarv1alpha1.RenderTask{}
416✔
83
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
423✔
84
                if apierrors.IsNotFound(err) {
14✔
85
                        return ctrlResult, nil
7✔
86
                }
7✔
87

88
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
89
        }
90

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

×
96
                return ctrlResult, nil
×
97
        }
×
98

99
        // Check if renderjob has already completed successfully
100
        sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobSucceeded)
409✔
101
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
414✔
102
                log.V(1).Info("RenderTask has already completed successfully, no further action needed")
5✔
103

5✔
104
                return ctrlResult, nil
5✔
105
        }
5✔
106

107
        // Determine the namespace for Jobs/Secrets — use the RenderTask's namespace
108
        jobNS := r.taskNamespace(res)
404✔
109

404✔
110
        // Reconcile Config Secret
404✔
111
        configSecret := &corev1.Secret{}
404✔
112
        err := r.Get(ctx, r.configSecretKey(res, jobNS), configSecret)
404✔
113
        if err != nil && apierrors.IsNotFound(err) {
581✔
114
                createdSecret, err := r.createConfigSecret(ctx, res, jobNS)
177✔
115
                if err != nil {
177✔
116
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateSecretFailed", "CreateConfigSecret", "Failed to create config secret: %s", err)
×
117

×
118
                        return ctrlResult, errLogAndWrap(log, err, "failed to create secret")
×
119
                }
×
120

121
                configSecret = createdSecret
177✔
122
        } else if err != nil {
227✔
123
                return ctrlResult, errLogAndWrap(log, err, "could not get secret")
×
124
        }
×
125

126
        // Resolve push secret from the RenderTask's PushSecretRef
127
        var pushSecret *corev1.Secret
404✔
128
        if res.Spec.PushSecretRef != nil {
806✔
129
                pushSecret = &corev1.Secret{}
402✔
130
                if err := r.Get(ctx, client.ObjectKey{Name: res.Spec.PushSecretRef.Name, Namespace: jobNS}, pushSecret); err != nil {
630✔
131
                        return ctrlResult, errLogAndWrap(log, err, "failed to get push secret")
228✔
132
                }
228✔
133
        }
134

135
        // Reconcile Job
136
        job := &batchv1.Job{}
176✔
137
        err = r.Get(ctx, r.renderJobKey(res, jobNS), job)
176✔
138
        if err != nil && apierrors.IsNotFound(err) {
194✔
139
                err := r.createRenderJob(ctx, res, configSecret, pushSecret, jobNS)
18✔
140
                if err != nil {
18✔
141
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateJobFailed", "CreateJob", "Failed to create job: %s", err)
×
142

×
143
                        return ctrlResult, errLogAndWrap(log, err, "failed to create job")
×
144
                }
×
145
        } else if err != nil {
158✔
146
                return ctrlResult, errLogAndWrap(log, err, "could not get job")
×
147
        }
×
148

149
        // Update Status
150
        if changed := r.updateResourceStatusFromJob(ctx, res, job); changed {
200✔
151
                if err := r.Status().Update(ctx, res); err != nil {
25✔
152
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
1✔
153
                }
1✔
154
        }
155

156
        ttlDuration := time.Duration(ttlSeconds(res.Spec.FailedJobTTL)) * time.Second
175✔
157

175✔
158
        switch {
175✔
159
        case job.Status.Succeeded > 0:
3✔
160
                cleanupRenderResources(ctx, r, res, job, jobNS)
3✔
161
                log.V(1).Info("Cleaned up after successful job")
3✔
162

3✔
163
                return ctrlResult, nil
3✔
164

165
        case job.Status.Failed > 0:
138✔
166
                if shouldCleanupSecrets(res, ttlDuration) {
272✔
167
                        cleanupSecrets(ctx, r, res, jobNS)
134✔
168
                        log.V(1).Info("Cleaned up secrets after failed job TTL")
134✔
169

134✔
170
                        return ctrlResult, nil
134✔
171
                }
134✔
172

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

4✔
176
                return ctrl.Result{RequeueAfter: remaining + time.Second}, nil
4✔
177
        }
178

179
        return ctrlResult, nil
34✔
180
}
181

182
// taskNamespace returns the namespace to use for Jobs/Secrets.
183
func (r *RenderTaskReconciler) taskNamespace(res *solarv1alpha1.RenderTask) string {
404✔
184
        return res.Namespace
404✔
185
}
404✔
186

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

176✔
191
        if job == nil {
176✔
192
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
×
193
                        Type:               ConditionTypeJobScheduled,
×
194
                        Status:             metav1.ConditionFalse,
×
195
                        ObservedGeneration: res.Generation,
×
196
                        Reason:             "DoesNotExist",
×
197
                        Message:            "Renderer job does not exist",
×
198
                })
×
199

×
200
                return changed
×
201
        }
×
202

203
        if job.Status.Succeeded > 0 {
179✔
204
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
3✔
205
                        Type:               ConditionTypeJobSucceeded,
3✔
206
                        Status:             metav1.ConditionTrue,
3✔
207
                        ObservedGeneration: res.Generation,
3✔
208
                        Reason:             "JobSucceeded",
3✔
209
                        Message:            fmt.Sprintf("Renderer job completed successfully at %v", job.Status.CompletionTime),
3✔
210
                })
3✔
211

3✔
212
                chartURL := r.reference(res.Spec.BaseURL, res.Spec.Repository, res.Spec.Tag)
3✔
213
                if res.Status.ChartURL != chartURL {
6✔
214
                        res.Status.ChartURL = chartURL
3✔
215
                        changed = true
3✔
216
                }
3✔
217

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

3✔
221
                return changed
3✔
222
        }
223

224
        if job.Status.Failed > 0 {
311✔
225
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
138✔
226
                        Type:               ConditionTypeJobFailed,
138✔
227
                        Status:             metav1.ConditionTrue,
138✔
228
                        ObservedGeneration: res.Generation,
138✔
229
                        Reason:             "JobFailed",
138✔
230
                        Message:            "Renderer job failed",
138✔
231
                })
138✔
232
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "JobFailed", "RunJob", "Renderer job failed")
138✔
233
                log.V(1).Info("Job failed", "name", job.Name)
138✔
234

138✔
235
                return changed
138✔
236
        }
138✔
237

238
        return apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
35✔
239
                Type:               ConditionTypeJobScheduled,
35✔
240
                Status:             metav1.ConditionTrue,
35✔
241
                ObservedGeneration: res.Generation,
35✔
242
                Reason:             "JobScheduled",
35✔
243
                Message:            fmt.Sprintf("Renderer job is running (active: %d, succeeded: %d, failed: %d)", job.Status.Active, job.Status.Succeeded, job.Status.Failed),
35✔
244
        })
35✔
245
}
246

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

253
        return r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
3✔
254
}
255

256
func (r *RenderTaskReconciler) deleteConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) error {
137✔
257
        secret := &corev1.Secret{}
137✔
258
        if err := r.Get(ctx, r.configSecretKey(res, jobNS), secret); err != nil {
137✔
UNCOV
259
                return err
×
UNCOV
260
        }
×
261

262
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
137✔
263
}
264

265
func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, configSecret, pushSecret *corev1.Secret, jobNS string) error {
18✔
266
        log := ctrl.LoggerFrom(ctx)
18✔
267

18✔
268
        jobKey := r.renderJobKey(res, jobNS)
18✔
269
        jobName := jobKey.Name
18✔
270
        backoffLimit := int32(3)
18✔
271
        ttlSecondsAfterFinished := int32(3600)
18✔
272
        if res.Spec.FailedJobTTL != nil {
20✔
273
                ttlSecondsAfterFinished = *res.Spec.FailedJobTTL
2✔
274
        }
2✔
275

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

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

347
        pushURL := r.reference(res.Spec.BaseURL, res.Spec.Repository, res.Spec.Tag)
18✔
348

18✔
349
        args := slices.Clone(r.RendererArgs)
18✔
350
        args = append(args, "/etc/renderer/config.json", fmt.Sprintf("--url=%s", pushURL))
18✔
351
        if res.Spec.PlainHTTP {
18✔
352
                args = append(args, "--plain-http=true")
×
353
        }
×
354

355
        job := &batchv1.Job{
18✔
356
                ObjectMeta: metav1.ObjectMeta{
18✔
357
                        Name:      jobName,
18✔
358
                        Namespace: jobKey.Namespace,
18✔
359
                        Annotations: map[string]string{
18✔
360
                                annotationJobName: jobName,
18✔
361
                        },
18✔
362
                },
18✔
363
                Spec: batchv1.JobSpec{
18✔
364
                        BackoffLimit:            &backoffLimit,
18✔
365
                        TTLSecondsAfterFinished: &ttlSecondsAfterFinished,
18✔
366
                        Template: corev1.PodTemplateSpec{
18✔
367
                                Spec: corev1.PodSpec{
18✔
368
                                        RestartPolicy: corev1.RestartPolicyNever,
18✔
369
                                        Containers: []corev1.Container{
18✔
370
                                                {
18✔
371
                                                        Name:         "renderer",
18✔
372
                                                        Image:        r.RendererImage,
18✔
373
                                                        Command:      []string{r.RendererCommand},
18✔
374
                                                        Args:         args,
18✔
375
                                                        Env:          envVars,
18✔
376
                                                        VolumeMounts: volumeMounts,
18✔
377
                                                },
18✔
378
                                        },
18✔
379
                                        Volumes: volumes,
18✔
380
                                },
18✔
381
                        },
18✔
382
                },
18✔
383
        }
18✔
384

18✔
385
        if pushSecret != nil {
34✔
386
                switch pushSecret.Type {
16✔
387
                case corev1.SecretTypeBasicAuth:
1✔
388
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env,
1✔
389
                                corev1.EnvVar{
1✔
390
                                        Name: "REGISTRY_USERNAME",
1✔
391
                                        ValueFrom: &corev1.EnvVarSource{
1✔
392
                                                SecretKeyRef: &corev1.SecretKeySelector{
1✔
393
                                                        LocalObjectReference: corev1.LocalObjectReference{
1✔
394
                                                                Name: pushSecret.Name,
1✔
395
                                                        },
1✔
396
                                                        Key: "username",
1✔
397
                                                },
1✔
398
                                        },
1✔
399
                                },
1✔
400
                                corev1.EnvVar{
1✔
401
                                        Name: "REGISTRY_PASSWORD",
1✔
402
                                        ValueFrom: &corev1.EnvVarSource{
1✔
403
                                                SecretKeyRef: &corev1.SecretKeySelector{
1✔
404
                                                        LocalObjectReference: corev1.LocalObjectReference{
1✔
405
                                                                Name: pushSecret.Name,
1✔
406
                                                        },
1✔
407
                                                        Key: "password",
1✔
408
                                                },
1✔
409
                                        },
1✔
410
                                },
1✔
411
                        )
1✔
412

413
                case corev1.SecretTypeDockerConfigJson:
1✔
414
                        job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
1✔
415
                                Name: "dockerconfig",
1✔
416
                                VolumeSource: corev1.VolumeSource{
1✔
417
                                        Secret: &corev1.SecretVolumeSource{
1✔
418
                                                SecretName: pushSecret.Name,
1✔
419
                                                Items: []corev1.KeyToPath{
1✔
420
                                                        {
1✔
421
                                                                Key:  ".dockerconfigjson",
1✔
422
                                                                Path: "dockerconfig.json",
1✔
423
                                                        },
1✔
424
                                                },
1✔
425
                                        },
1✔
426
                                },
1✔
427
                        })
1✔
428

1✔
429
                        job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
1✔
430
                                Name:      "dockerconfig",
1✔
431
                                MountPath: "/etc/renderer/dockerconfig.json",
1✔
432
                                SubPath:   "dockerconfig.json",
1✔
433
                                ReadOnly:  true,
1✔
434
                        })
1✔
435

1✔
436
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{
1✔
437
                                Name:  "DOCKER_CONFIG",
1✔
438
                                Value: "/etc/renderer/dockerconfig.json",
1✔
439
                        })
1✔
440
                default:
14✔
441
                }
442
        }
443

444
        if len(r.RendererImagePullSecrets) > 0 {
19✔
445
                refs := make([]corev1.LocalObjectReference, len(r.RendererImagePullSecrets))
1✔
446
                for i, n := range r.RendererImagePullSecrets {
3✔
447
                        refs[i] = corev1.LocalObjectReference{Name: n}
2✔
448
                }
2✔
449
                job.Spec.Template.Spec.ImagePullSecrets = refs
1✔
450
        }
451

452
        // Set owner references
453
        if err := controllerutil.SetControllerReference(res, job, r.Scheme); err != nil {
18✔
454
                return errLogAndWrap(log, err, "failed to set controller reference")
×
455
        }
×
456

457
        if err := r.Create(ctx, job); err != nil {
18✔
458
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create job: %s", err)
×
459

×
460
                return errLogAndWrap(log, err, "job creation failed")
×
461
        }
×
462

463
        res.Status.JobRef = &corev1.ObjectReference{
18✔
464
                APIVersion: batchv1.SchemeGroupVersion.String(),
18✔
465
                Kind:       "Job",
18✔
466
                Namespace:  job.Namespace,
18✔
467
                Name:       job.Name,
18✔
468
        }
18✔
469

18✔
470
        if err := r.Status().Update(ctx, res); err != nil {
18✔
471
                return errLogAndWrap(log, err, "failed to update status")
×
472
        }
×
473

474
        return nil
18✔
475
}
476

477
func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) (*corev1.Secret, error) {
177✔
478
        log := ctrl.LoggerFrom(ctx)
177✔
479

177✔
480
        cfgJson, err := json.Marshal(res.Spec.RendererConfig)
177✔
481
        if err != nil {
177✔
482
                return nil, err
×
483
        }
×
484

485
        secretKey := r.configSecretKey(res, jobNS)
177✔
486
        secret := &corev1.Secret{
177✔
487
                ObjectMeta: metav1.ObjectMeta{
177✔
488
                        Name:      secretKey.Name,
177✔
489
                        Namespace: secretKey.Namespace,
177✔
490
                        Annotations: map[string]string{
177✔
491
                                annotationSecretName: secretKey.Name,
177✔
492
                        },
177✔
493
                },
177✔
494
                Type: corev1.SecretTypeOpaque,
177✔
495
                Data: map[string][]byte{
177✔
496
                        "config.json": cfgJson,
177✔
497
                },
177✔
498
        }
177✔
499

177✔
500
        // Set owner references
177✔
501
        if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil {
177✔
502
                return nil, errLogAndWrap(log, err, "failed to set controller reference")
×
503
        }
×
504

505
        if err := r.Create(ctx, secret); err != nil {
177✔
506
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
×
507

×
508
                return nil, errLogAndWrap(log, err, "secret creation failed")
×
509
        }
×
510

511
        res.Status.ConfigSecretRef = &corev1.ObjectReference{
177✔
512
                APIVersion: corev1.SchemeGroupVersion.String(),
177✔
513
                Kind:       "Secret",
177✔
514
                Namespace:  secret.Namespace,
177✔
515
                Name:       secret.Name,
177✔
516
        }
177✔
517

177✔
518
        if err := r.Status().Update(ctx, res); err != nil {
177✔
519
                return nil, errLogAndWrap(log, err, "failed to update status")
×
520
        }
×
521

522
        return secret, nil
177✔
523
}
524

525
func (r *RenderTaskReconciler) configSecretKey(res *solarv1alpha1.RenderTask, jobNS string) client.ObjectKey {
718✔
526
        return client.ObjectKey{
718✔
527
                Name:      truncateName(fmt.Sprintf("render-%s", res.Name), maxK8sLabelValueLen),
718✔
528
                Namespace: jobNS,
718✔
529
        }
718✔
530
}
718✔
531

532
func (r *RenderTaskReconciler) renderJobKey(res *solarv1alpha1.RenderTask, jobNS string) client.ObjectKey {
197✔
533
        return client.ObjectKey{
197✔
534
                Name:      truncateName(fmt.Sprintf("render-%s", res.Name), maxK8sLabelValueLen),
197✔
535
                Namespace: jobNS,
197✔
536
        }
197✔
537
}
197✔
538

539
func (r *RenderTaskReconciler) reference(baseURL, repo, tag string) string {
21✔
540
        base := baseURL
21✔
541
        if !strings.HasPrefix(base, "oci://") {
40✔
542
                base = fmt.Sprintf("oci://%s", base)
19✔
543
        }
19✔
544

545
        base = strings.TrimSuffix(base, "/")
21✔
546

21✔
547
        return fmt.Sprintf("%s/%s:%s", base, repo, tag)
21✔
548
}
549

550
func ttlSeconds(ttl *int32) int32 {
175✔
551
        if ttl != nil {
315✔
552
                return *ttl
140✔
553
        }
140✔
554

555
        return 3600
35✔
556
}
557

558
func shouldCleanupSecrets(res *solarv1alpha1.RenderTask, ttl time.Duration) bool {
138✔
559
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
138✔
560

138✔
561
        return cond != nil && time.Since(cond.LastTransitionTime.Time) >= ttl
138✔
562
}
138✔
563

564
func remainingTTL(res *solarv1alpha1.RenderTask, ttl time.Duration) time.Duration {
4✔
565
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
4✔
566
        if cond == nil {
4✔
567
                return ttl
×
568
        }
×
569

570
        remaining := ttl - time.Since(cond.LastTransitionTime.Time)
4✔
571
        if remaining < 0 {
4✔
572
                return 0
×
573
        }
×
574

575
        return remaining
4✔
576
}
577

578
func cleanupSecrets(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, jobNS string) {
137✔
579
        if err := r.deleteConfigSecret(ctx, res, jobNS); err != nil && !apierrors.IsNotFound(err) {
137✔
580
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete config secret: %s", err)
×
581
        }
×
582
}
583

584
func cleanupRenderResources(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, job *batchv1.Job, jobNS string) {
3✔
585
        cleanupSecrets(ctx, r, res, jobNS)
3✔
586
        if err := r.deleteRenderJob(ctx, res, jobNS); err != nil && !apierrors.IsNotFound(err) {
3✔
587
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete job: %s", err)
×
588
        }
×
589
}
590

591
// SetupWithManager sets up the controller with the Manager.
592
func (r *RenderTaskReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
593
        return ctrl.NewControllerManagedBy(mgr).
1✔
594
                For(&solarv1alpha1.RenderTask{}).
1✔
595
                Owns(&batchv1.Job{}).
1✔
596
                Owns(&corev1.Secret{}).
1✔
597
                Complete(r)
1✔
598
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc