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

opendefensecloud / solution-arsenal / 35553136166

21 Sep 2026 02:08AM UTC coverage: 78.534%. Remained the same
35553136166

push

github

web-flow
fix(deps): update dependencies (patch & digest) (#816)

This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) | Type |
Update | Pending |
|---|---|---|---|---|---|---|
| [@tanstack/react-router](https://tanstack.com/router)
([source](https://redirect.github.com/TanStack/router/tree/HEAD/packages/react-router))
| [`1.170.36` →
`1.170.38`](https://renovatebot.com/diffs/npm/@tanstack%2freact-router/1.170.36/1.170.38)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@tanstack%2freact-router/1.170.38?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tanstack%2freact-router/1.170.36/1.170.38?slim=true)
| dependencies | patch | |
|
[eslint-plugin-react-refresh](https://redirect.github.com/ArnaudBarre/eslint-plugin-react-refresh)
| [`0.5.6` →
`0.5.7`](https://renovatebot.com/diffs/npm/eslint-plugin-react-refresh/0.5.6/0.5.7)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-react-refresh/0.5.7?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-react-refresh/0.5.6/0.5.7?slim=true)
| devDependencies | patch | |
| [github.com/onsi/gomega](https://redirect.github.com/onsi/gomega) |
`v1.43.0` → `v1.43.1` |
![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fonsi%2fgomega/v1.43.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fonsi%2fgomega/v1.43.0/v1.43.1?slim=true)
| require | patch | |
| [node](https://hub.docker.com/_/node)
([source](https://redirect.github.com/nodejs/node)) | `333f6b3` →
`83f1c38` | | | stage | digest | |
| [prettier](https://prettier.io)
([source](https://redirect.github.com/prettier/prettier)) | [`3.9.6` →
`3.9.8`](https://renovatebot.com/diffs/npm/prettier/3.9.6/3.9.8) |
![age](https://developer.mend.io/api/mc/badges/age/npm/prettier/3.9.8?slim=true)
|
... (continued)

5078 of 6466 relevant lines covered (78.53%)

33.66 hits per line

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

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

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

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

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

88
                return ctrlResult, fmt.Errorf("failed to get RenderTask: %w", err)
×
89
        }
90

91
        // RenderTask instance marked for deletion, stop reconciling
92
        if !res.DeletionTimestamp.IsZero() {
880 ✔
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)
880 ✔
101
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
880 ✔
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)
875 ✔
109

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

×
118
                        return ctrlResult, fmt.Errorf("failed to create secret: %w", err)
×
119
                }
×
120

121
                configSecret = createdSecret
605 ✔
122
        } else if err != nil {
270 ✔
123
                return ctrlResult, fmt.Errorf("failed to get config secret: %w", err)
×
124
        }
×
125

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

135
        // Resolve source secret from the RenderTask's SourceSecretRef. It holds the
136
        // credentials for reading the OCM component the release is built from.
137
        var sourceSecret *corev1.Secret
647 ✔
138
        if res.Spec.SourceSecretRef != nil {
647 ✔
139
                sourceSecret = &corev1.Secret{}
7 ✔
140
                if err := r.Get(ctx, client.ObjectKey{Name: res.Spec.SourceSecretRef.Name, Namespace: jobNS}, sourceSecret); err != nil {
7 ✔
141
                        return ctrlResult, fmt.Errorf("failed to get source secret: %w", err)
×
142
                }
×
143
        }
144

145
        // Reconcile Job
146
        job := &batchv1.Job{}
647 ✔
147
        err = r.Get(ctx, r.renderJobKey(res, jobNS), job)
647 ✔
148
        if err != nil && apierrors.IsNotFound(err) {
647 ✔
149
                err := r.createRenderJob(ctx, res, configSecret, pushSecret, sourceSecret, jobNS)
26 ✔
150
                if err != nil {
26 ✔
151
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateJobFailed", "CreateJob", "Failed to create job: %s", err)
×
152

×
153
                        return ctrlResult, fmt.Errorf("failed to create job: %w", err)
×
154
                }
×
155
        } else if err != nil {
621 ✔
156
                return ctrlResult, fmt.Errorf("failed to get job: %w", err)
×
157
        }
×
158

159
        // Update Status
160
        if changed := r.updateResourceStatusFromJob(ctx, res, job); changed {
647 ✔
161
                if err := r.Status().Update(ctx, res); err != nil {
31 ✔
162
                        return ctrlResult, fmt.Errorf("failed to update status: %w", err)
×
163
                }
×
164
        }
165

166
        ttlDuration := time.Duration(ttlSeconds(res.Spec.FailedJobTTL)) * time.Second
647 ✔
167

647 ✔
168
        switch {
647 ✔
169
        case job.Status.Succeeded > 0:
170
                cleanupRenderResources(ctx, r, res, job, jobNS)
3 ✔
171
                log.V(1).Info("Cleaned up after successful job")
3 ✔
172

3 ✔
173
                return ctrlResult, nil
3 ✔
174

175
        case job.Status.Failed > 0:
176
                if shouldCleanupSecrets(res, ttlDuration) {
600 ✔
177
                        cleanupSecrets(ctx, r, res, jobNS)
596 ✔
178
                        log.V(1).Info("Cleaned up secrets after failed job TTL")
596 ✔
179

596 ✔
180
                        return ctrlResult, nil
596 ✔
181
                }
596 ✔
182

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

4 ✔
186
                return ctrl.Result{RequeueAfter: remaining + time.Second}, nil
4 ✔
187
        }
188

189
        return ctrlResult, nil
44 ✔
190
}
191

192
// taskNamespace returns the namespace to use for Jobs/Secrets.
193
func (r *RenderTaskReconciler) taskNamespace(res *solarv1alpha1.RenderTask) string {
194
        return res.Namespace
875 ✔
195
}
875 ✔
196

197
// updateResourceStatusFromJob updates the resource status based on job status
198
func (r *RenderTaskReconciler) updateResourceStatusFromJob(ctx context.Context, res *solarv1alpha1.RenderTask, job *batchv1.Job) (changed bool) {
199
        log := ctrl.LoggerFrom(ctx)
647 ✔
200

647 ✔
201
        if job == nil {
647 ✔
202
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
×
203
                        Type:               ConditionTypeJobScheduled,
×
204
                        Status:             metav1.ConditionFalse,
×
205
                        ObservedGeneration: res.Generation,
×
206
                        Reason:             "DoesNotExist",
×
207
                        Message:            "Renderer job does not exist",
×
208
                })
×
209

×
210
                return changed
×
211
        }
×
212

213
        if job.Status.Succeeded > 0 {
647 ✔
214
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
3 ✔
215
                        Type:               ConditionTypeJobSucceeded,
3 ✔
216
                        Status:             metav1.ConditionTrue,
3 ✔
217
                        ObservedGeneration: res.Generation,
3 ✔
218
                        Reason:             "JobSucceeded",
3 ✔
219
                        Message:            fmt.Sprintf("Renderer job completed successfully at %v", job.Status.CompletionTime),
3 ✔
220
                })
3 ✔
221

3 ✔
222
                chartURL := r.reference(res.Spec.BaseURL, res.Spec.Repository, res.Spec.Tag)
3 ✔
223
                if res.Status.ChartURL != chartURL {
3 ✔
224
                        res.Status.ChartURL = chartURL
3 ✔
225
                        changed = true
3 ✔
226
                }
3 ✔
227

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

3 ✔
231
                return changed
3 ✔
232
        }
233

234
        if job.Status.Failed > 0 {
644 ✔
235
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
600 ✔
236
                        Type:               ConditionTypeJobFailed,
600 ✔
237
                        Status:             metav1.ConditionTrue,
600 ✔
238
                        ObservedGeneration: res.Generation,
600 ✔
239
                        Reason:             "JobFailed",
600 ✔
240
                        Message:            "Renderer job failed",
600 ✔
241
                })
600 ✔
242
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "JobFailed", "RunJob", "Renderer job failed")
600 ✔
243
                log.V(1).Info("Job failed", "name", job.Name)
600 ✔
244

600 ✔
245
                return changed
600 ✔
246
        }
600 ✔
247

248
        return apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
44 ✔
249
                Type:               ConditionTypeJobScheduled,
44 ✔
250
                Status:             metav1.ConditionTrue,
44 ✔
251
                ObservedGeneration: res.Generation,
44 ✔
252
                Reason:             "JobScheduled",
44 ✔
253
                Message:            fmt.Sprintf("Renderer job is running (active: %d, succeeded: %d, failed: %d)", job.Status.Active, job.Status.Succeeded, job.Status.Failed),
44 ✔
254
        })
44 ✔
255
}
256

257
func (r *RenderTaskReconciler) deleteRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) error {
258
        job := &batchv1.Job{}
3 ✔
259
        if err := r.Get(ctx, r.renderJobKey(res, jobNS), job); err != nil {
3 ✔
260
                return err
×
261
        }
×
262

263
        return r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
3 ✔
264
}
265

266
func (r *RenderTaskReconciler) deleteConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) error {
267
        secret := &corev1.Secret{}
599 ✔
268
        if err := r.Get(ctx, r.configSecretKey(res, jobNS), secret); err != nil {
599 ✔
269
                return err
18 ✔
270
        }
18 ✔
271

272
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
581 ✔
273
}
274

275
func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, configSecret, pushSecret, sourceSecret *corev1.Secret, jobNS string) error {
276
        jobKey := r.renderJobKey(res, jobNS)
26 ✔
277
        jobName := jobKey.Name
26 ✔
278
        backoffLimit := int32(3)
26 ✔
279
        ttlSecondsAfterFinished := int32(3600)
26 ✔
280
        if res.Spec.FailedJobTTL != nil {
26 ✔
281
                ttlSecondsAfterFinished = *res.Spec.FailedJobTTL
2 ✔
282
        }
2 ✔
283

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

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

349
        pushURL := r.reference(res.Spec.BaseURL, res.Spec.Repository, res.Spec.Tag)
26 ✔
350

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

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

385
        if pushSecret != nil {
26 ✔
386
                switch pushSecret.Type {
16 ✔
387
                case corev1.SecretTypeBasicAuth:
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
                                                        Name: pushSecret.Name,
1 ✔
394
                                                        Key:  "username",
1 ✔
395
                                                },
1 ✔
396
                                        },
1 ✔
397
                                },
1 ✔
398
                                corev1.EnvVar{
1 ✔
399
                                        Name: "REGISTRY_PASSWORD",
1 ✔
400
                                        ValueFrom: &corev1.EnvVarSource{
1 ✔
401
                                                SecretKeyRef: &corev1.SecretKeySelector{
1 ✔
402
                                                        Name: pushSecret.Name,
1 ✔
403
                                                        Key:  "password",
1 ✔
404
                                                },
1 ✔
405
                                        },
1 ✔
406
                                },
1 ✔
407
                        )
1 ✔
408

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

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

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

438
        // Credentials for reading the OCM component. Kept separate from the push
439
        // credentials because the source registry is frequently a different one.
440
        switch {
26 ✔
441
        case hasBasicAuthKeys(sourceSecret):
442
                job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env,
2 ✔
443
                        corev1.EnvVar{
2 ✔
444
                                Name: "SOURCE_REGISTRY_USERNAME",
2 ✔
445
                                ValueFrom: &corev1.EnvVarSource{
2 ✔
446
                                        SecretKeyRef: &corev1.SecretKeySelector{
2 ✔
447
                                                Name: sourceSecret.Name,
2 ✔
448
                                                Key:  secretKeyUsername,
2 ✔
449
                                        },
2 ✔
450
                                },
2 ✔
451
                        },
2 ✔
452
                        corev1.EnvVar{
2 ✔
453
                                Name: "SOURCE_REGISTRY_PASSWORD",
2 ✔
454
                                ValueFrom: &corev1.EnvVarSource{
2 ✔
455
                                        SecretKeyRef: &corev1.SecretKeySelector{
2 ✔
456
                                                Name: sourceSecret.Name,
2 ✔
457
                                                Key:  secretKeyPassword,
2 ✔
458
                                        },
2 ✔
459
                                },
2 ✔
460
                        },
2 ✔
461
                )
2 ✔
462

463
        case hasDockerConfigJSON(sourceSecret):
464
                // Mounted at its own path so it cannot collide with the push secret's
465
                // docker config
466
                job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
1 ✔
467
                        Name: "source-dockerconfig",
1 ✔
468
                        Secret: &corev1.SecretVolumeSource{
1 ✔
469
                                SecretName: sourceSecret.Name,
1 ✔
470
                                Items: []corev1.KeyToPath{
1 ✔
471
                                        {
1 ✔
472
                                                Key:  corev1.DockerConfigJsonKey,
1 ✔
473
                                                Path: "config.json",
1 ✔
474
                                        },
1 ✔
475
                                },
1 ✔
476
                        },
1 ✔
477
                })
1 ✔
478

1 ✔
479
                job.Spec.Template.Spec.Containers[0].VolumeMounts = append(
1 ✔
480
                        job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
1 ✔
481
                                Name:      "source-dockerconfig",
1 ✔
482
                                MountPath: sourceDockerConfigPath,
1 ✔
483
                                SubPath:   "config.json",
1 ✔
484
                                ReadOnly:  true,
1 ✔
485
                        })
1 ✔
486

1 ✔
487
                job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env,
1 ✔
488
                        corev1.EnvVar{
1 ✔
489
                                Name:  "SOURCE_DOCKER_CONFIG",
1 ✔
490
                                Value: sourceDockerConfigPath,
1 ✔
491
                        })
1 ✔
492
        }
493

494
        if len(r.RendererImagePullSecrets) > 0 {
26 ✔
495
                refs := make([]corev1.LocalObjectReference, len(r.RendererImagePullSecrets))
1 ✔
496
                for i, n := range r.RendererImagePullSecrets {
1 ✔
497
                        refs[i] = corev1.LocalObjectReference{Name: n}
2 ✔
498
                }
2 ✔
499
                job.Spec.Template.Spec.ImagePullSecrets = refs
1 ✔
500
        }
501

502
        // Set owner references
503
        if err := controllerutil.SetControllerReference(res, job, r.Scheme); err != nil {
26 ✔
504
                return fmt.Errorf("failed to set controller reference on Job: %w", err)
×
505
        }
×
506

507
        if err := r.Create(ctx, job); err != nil {
26 ✔
508
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create job: %s", err)
×
509

×
510
                return err
×
511
        }
×
512

513
        res.Status.JobRef = &corev1.ObjectReference{
26 ✔
514
                APIVersion: batchv1.SchemeGroupVersion.String(),
26 ✔
515
                Kind:       "Job",
26 ✔
516
                Namespace:  job.Namespace,
26 ✔
517
                Name:       job.Name,
26 ✔
518
        }
26 ✔
519

520
        if err := r.Status().Update(ctx, res); err != nil {
26 ✔
521
                return fmt.Errorf("failed to update status: %w", err)
×
522
        }
×
523

524
        return nil
26 ✔
525
}
526

527
// secretKeyUsername and secretKeyPassword are the keys SolAr reads from a
528
// Registry's solarSecretRef, mirroring pkg/discovery/registry_provider.go.
529
const (
530
        secretKeyUsername = "username"
531
        secretKeyPassword = "password"
532

533
        // sourceDockerConfigPath is where a dockerconfigjson source secret is
534
        // mounted in the render Pod, kept distinct from the push secret's mount.
535
        sourceDockerConfigPath = "/etc/renderer/source-dockerconfig.json"
536
)
537

538
// hasBasicAuthKeys reports whether secret carries both credential keys with
539
// non-empty values, regardless of its declared Secret type. Empty values are
540
// rejected
541
func hasBasicAuthKeys(secret *corev1.Secret) bool {
542
        if secret == nil {
26 ✔
543
                return false
19 ✔
544
        }
19 ✔
545

546
        username, hasUser := secret.Data[secretKeyUsername]
7 ✔
547
        password, hasPass := secret.Data[secretKeyPassword]
7 ✔
548

7 ✔
549
        return hasUser && hasPass && len(username) > 0 && len(password) > 0
7 ✔
550
}
551

552
// hasDockerConfigJSON reports whether secret carries a non-empty docker config,
553
// the other shape a Registry's solarSecretRef can take.
554
func hasDockerConfigJSON(secret *corev1.Secret) bool {
555
        if secret == nil {
24 ✔
556
                return false
19 ✔
557
        }
19 ✔
558

559
        config, ok := secret.Data[corev1.DockerConfigJsonKey]
5 ✔
560

5 ✔
561
        return ok && len(config) > 0
5 ✔
562
}
563

564
func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) (*corev1.Secret, error) {
565
        cfgJson, err := json.Marshal(res.Spec.RendererConfig)
605 ✔
566
        if err != nil {
605 ✔
567
                return nil, err
×
568
        }
×
569

570
        secretKey := r.configSecretKey(res, jobNS)
605 ✔
571
        secret := &corev1.Secret{
605 ✔
572
                Name:      secretKey.Name,
605 ✔
573
                Namespace: secretKey.Namespace,
605 ✔
574
                Annotations: map[string]string{
605 ✔
575
                        annotationSecretName: secretKey.Name,
605 ✔
576
                },
605 ✔
577
                Type: corev1.SecretTypeOpaque,
605 ✔
578
                Data: map[string][]byte{
605 ✔
579
                        "config.json": cfgJson,
605 ✔
580
                },
605 ✔
581
        }
605 ✔
582

583
        // Set owner references
584
        if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil {
605 ✔
585
                return nil, fmt.Errorf("failed to set controller reference on Secret: %w", err)
×
586
        }
×
587

588
        if err := r.Create(ctx, secret); err != nil {
605 ✔
589
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
×
590

×
591
                return nil, err
×
592
        }
×
593

594
        res.Status.ConfigSecretRef = &corev1.ObjectReference{
605 ✔
595
                APIVersion: corev1.SchemeGroupVersion.String(),
605 ✔
596
                Kind:       "Secret",
605 ✔
597
                Namespace:  secret.Namespace,
605 ✔
598
                Name:       secret.Name,
605 ✔
599
        }
605 ✔
600

601
        if err := r.Status().Update(ctx, res); err != nil {
605 ✔
602
                return nil, fmt.Errorf("failed to update status: %w", err)
×
603
        }
×
604

605
        return secret, nil
605 ✔
606
}
607

608
func (r *RenderTaskReconciler) configSecretKey(res *solarv1alpha1.RenderTask, jobNS string) client.ObjectKey {
609
        return client.ObjectKey{
2,079 ✔
610
                Name:      truncateName(fmt.Sprintf("render-%s", res.Name), maxK8sLabelValueLen),
2,079 ✔
611
                Namespace: jobNS,
2,079 ✔
612
        }
2,079 ✔
613
}
614

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

622
func (r *RenderTaskReconciler) reference(baseURL, repo, tag string) string {
623
        base := baseURL
29 ✔
624
        if !strings.HasPrefix(base, "oci://") {
29 ✔
625
                base = fmt.Sprintf("oci://%s", base)
19 ✔
626
        }
19 ✔
627

628
        base = strings.TrimSuffix(base, "/")
29 ✔
629

29 ✔
630
        return fmt.Sprintf("%s/%s:%s", base, repo, tag)
29 ✔
631
}
632

633
func ttlSeconds(ttl *int32) int32 {
634
        if ttl != nil {
647 ✔
635
                return *ttl
602 ✔
636
        }
602 ✔
637

638
        return 3600
45 ✔
639
}
640

641
func shouldCleanupSecrets(res *solarv1alpha1.RenderTask, ttl time.Duration) bool {
642
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
600 ✔
643

600 ✔
644
        return cond != nil && time.Since(cond.LastTransitionTime.Time) >= ttl
600 ✔
645
}
600 ✔
646

647
func remainingTTL(res *solarv1alpha1.RenderTask, ttl time.Duration) time.Duration {
648
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
4 ✔
649
        if cond == nil {
4 ✔
650
                return ttl
×
651
        }
×
652

653
        remaining := ttl - time.Since(cond.LastTransitionTime.Time)
4 ✔
654
        if remaining < 0 {
4 ✔
655
                return 0
×
656
        }
×
657

658
        return remaining
4 ✔
659
}
660

661
func cleanupSecrets(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, jobNS string) {
662
        if err := r.deleteConfigSecret(ctx, res, jobNS); err != nil && !apierrors.IsNotFound(err) {
599 ✔
663
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete config secret: %s", err)
×
664
        }
×
665
}
666

667
func cleanupRenderResources(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, job *batchv1.Job, jobNS string) {
668
        cleanupSecrets(ctx, r, res, jobNS)
3 ✔
669
        if err := r.deleteRenderJob(ctx, res, jobNS); err != nil && !apierrors.IsNotFound(err) {
3 ✔
670
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete job: %s", err)
×
671
        }
×
672
}
673

674
// SetupWithManager sets up the controller with the Manager.
675
func (r *RenderTaskReconciler) SetupWithManager(mgr ctrl.Manager) error {
676
        return ctrl.NewControllerManagedBy(mgr).
1 ✔
677
                For(&solarv1alpha1.RenderTask{}).
1 ✔
678
                Owns(&batchv1.Job{}).
1 ✔
679
                Owns(&corev1.Secret{}).
1 ✔
680
                Complete(r)
1 ✔
681
}
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