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

opendefensecloud / solution-arsenal / 36888718399

01 Oct 2026 04:00PM UTC coverage: 78.611% (-0.02%) from 78.627%
36888718399

push

github

web-flow
chore(deps): update dependency pymdown-extensions to v12 (#826)

This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[pymdown-extensions](https://redirect.github.com/facelessuser/pymdown-extensions)
| `==11.0.2` → `==12.0.1` |
![age](https://developer.mend.io/api/mc/badges/age/pypi/pymdown-extensions/12.0.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/pymdown-extensions/11.0.2/12.0.1?slim=true)
|

---

### Release Notes

<details>
<summary>facelessuser/pymdown-extensions (pymdown-extensions)</summary>

###
[`v12.0.1`](https://redirect.github.com/facelessuser/pymdown-extensions/releases/tag/12.0.1)

[Compare
Source](https://redirect.github.com/facelessuser/pymdown-extensions/compare/12.0...12.0.1)

#### 12.0.1

- **FIX**: BetterEm: fix a case with new logic.

###
[`v12.0`](https://redirect.github.com/facelessuser/pymdown-extensions/releases/tag/12.0)

[Compare
Source](https://redirect.github.com/facelessuser/pymdown-extensions/compare/11.0.2...12.0)

#### 12.0

- **BREAK**: BetterEm: Complete rewrite. There may be some subtle
differences between legacy behavior and new
  CommonMark behavior.

- `*` and `_` emphasis are handled at the same time to provide more
natural nesting.
  - More performant.
  - CommonMark compliant.

- **BREAK**: Tilde, Caret, Mark: Rewritten and now built upon the latest
BetterEm logic.

- When used with BetterEm, Tilde, Caret, and Mark will be added to the
same delimiter processor under the hood and
will be parsed at the same time as emphasis, allowing for better nesting
logic.
- More performant. Performance issues reported by
([@&#8203;lexdotdev](https://redirect.github.com/lexdotdev)).
- Tilde, Caret, Mark: Allow deletion, insertions, and marking mid-word
by default. Set `smart_delete`,
`smart_insert`, or `smart_mark`, respectively, to enable... (continued)

5083 of 6466 relevant lines covered (78.61%)

27.05 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 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)
596 ✔
73
        ctrlResult := ctrl.Result{}
596 ✔
74

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

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

81
        // Fetch the RenderTask instance
82
        res := &solarv1alpha1.RenderTask{}
558 ✔
83
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
558 ✔
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() {
551 ✔
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)
551 ✔
101
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
551 ✔
102
                log.V(1).Info("RenderTask has already completed successfully, no further action needed")
6 ✔
103

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

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

545 ✔
110
        // Reconcile Config Secret
111
        configSecret := &corev1.Secret{}
545 ✔
112
        err := r.Get(ctx, r.configSecretKey(res, jobNS), configSecret)
545 ✔
113
        if err != nil && apierrors.IsNotFound(err) {
545 ✔
114
                createdSecret, err := r.createConfigSecret(ctx, res, jobNS)
292 ✔
115
                if err != nil {
292 ✔
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
292 ✔
122
        } else if err != nil {
253 ✔
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
545 ✔
128
        if res.Spec.PushSecretRef != nil {
545 ✔
129
                pushSecret = &corev1.Secret{}
535 ✔
130
                if err := r.Get(ctx, client.ObjectKey{Name: res.Spec.PushSecretRef.Name, Namespace: jobNS}, pushSecret); err != nil {
535 ✔
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
317 ✔
138
        if res.Spec.SourceSecretRef != nil {
317 ✔
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{}
317 ✔
147
        err = r.Get(ctx, r.renderJobKey(res, jobNS), job)
317 ✔
148
        if err != nil && apierrors.IsNotFound(err) {
317 ✔
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 {
291 ✔
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 {
317 ✔
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
317 ✔
167

317 ✔
168
        switch {
317 ✔
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) {
269 ✔
177
                        cleanupSecrets(ctx, r, res, jobNS)
265 ✔
178
                        log.V(1).Info("Cleaned up secrets after failed job TTL")
265 ✔
179

265 ✔
180
                        return ctrlResult, nil
265 ✔
181
                }
265 ✔
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
45 ✔
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
545 ✔
195
}
545 ✔
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)
317 ✔
200

317 ✔
201
        if job == nil {
317 ✔
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 {
317 ✔
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 {
314 ✔
235
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
269 ✔
236
                        Type:               ConditionTypeJobFailed,
269 ✔
237
                        Status:             metav1.ConditionTrue,
269 ✔
238
                        ObservedGeneration: res.Generation,
269 ✔
239
                        Reason:             "JobFailed",
269 ✔
240
                        Message:            "Renderer job failed",
269 ✔
241
                })
269 ✔
242
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "JobFailed", "RunJob", "Renderer job failed")
269 ✔
243
                log.V(1).Info("Job failed", "name", job.Name)
269 ✔
244

269 ✔
245
                return changed
269 ✔
246
        }
269 ✔
247

248
        return apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
45 ✔
249
                Type:               ConditionTypeJobScheduled,
45 ✔
250
                Status:             metav1.ConditionTrue,
45 ✔
251
                ObservedGeneration: res.Generation,
45 ✔
252
                Reason:             "JobScheduled",
45 ✔
253
                Message:            fmt.Sprintf("Renderer job is running (active: %d, succeeded: %d, failed: %d)", job.Status.Active, job.Status.Succeeded, job.Status.Failed),
45 ✔
254
        })
45 ✔
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{}
268 ✔
268
        if err := r.Get(ctx, r.configSecretKey(res, jobNS), secret); err != nil {
268 ✔
269
                return err
11 ✔
270
        }
11 ✔
271

272
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
257 ✔
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)
292 ✔
566
        if err != nil {
292 ✔
567
                return nil, err
×
568
        }
×
569

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

583
        // Set owner references
584
        if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil {
292 ✔
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 {
292 ✔
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{
292 ✔
595
                APIVersion: corev1.SchemeGroupVersion.String(),
292 ✔
596
                Kind:       "Secret",
292 ✔
597
                Namespace:  secret.Namespace,
292 ✔
598
                Name:       secret.Name,
292 ✔
599
        }
292 ✔
600

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

605
        return secret, nil
292 ✔
606
}
607

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

615
func (r *RenderTaskReconciler) renderJobKey(res *solarv1alpha1.RenderTask, jobNS string) client.ObjectKey {
616
        return client.ObjectKey{
346 ✔
617
                Name:      truncateName(fmt.Sprintf("render-%s", res.Name), maxK8sLabelValueLen),
346 ✔
618
                Namespace: jobNS,
346 ✔
619
        }
346 ✔
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 {
317 ✔
635
                return *ttl
271 ✔
636
        }
271 ✔
637

638
        return 3600
46 ✔
639
}
640

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

269 ✔
644
        return cond != nil && time.Since(cond.LastTransitionTime.Time) >= ttl
269 ✔
645
}
269 ✔
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) {
268 ✔
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