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

opendefensecloud / solution-arsenal / 39783242068

22 Oct 2026 01:01PM UTC coverage: 79.513% (-0.04%) from 79.557%
39783242068

push

github

web-flow
chore: clean up leftovers from the Component type removal (#883)

## What

Non-behavioral cleanup, mostly leftovers from removing the Component API
type (948c2e0c).

- `cmd/solar-discovery/main.go:29` -> help text still said it writes a
"Component or ComponentVersion", plus "coresponding"
- `pkg/controller/target_controller.go:58,701,709` -> dedup fallback
described as "the parent Component name from the CV", it is
`cv.Spec.ComponentName` (the OCM component name, see
`effectiveUniqueName` in `helpers.go:119`)
- `docs/developer-guide/rendering-pipeline.md:75` -> same phrase
- `test/e2e/e2e_test.go:221` -> "namesspaces"

## Why

Found while sweeping the Go code for nitpicks. The help text is
user-facing and points at a type that no longer exists.

Left alone on purpose:
- ADR 004 still says "parent Component name". Its a decision record, so
it keeps the wording of its time
- British spellings (`cancelled`, `labelled`, `behaviour`, ...), our
misspell config does not enforce a locale
- `gopls` reports nothing in hand-written code, only in generated
`client-go/` and `zz_generated*` files

## Testing

No behavior change, comments, one help string and one `By()` label.

```console
$ go build ./... && go vet ./cmd/solar-discovery ./pkg/controller && go vet -tags e2e ./test/e2e
$ echo $?
0
```

## Checklist
- [x] Tests added/updated, or confirmed that no tests are required (no
behavior change)
- [x] No breaking changes, or upgrade path documented above
- [x] Readable commit history (squashed and cleaned up as desired)
- [ ] AI code review considered and comments resolved

5387 of 6775 relevant lines covered (79.51%)

33.81 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
        // Condition reasons
41
        ReasonDoesNotExist = "DoesNotExist"
42
        ReasonJobSucceeded = "JobSucceeded"
43
        ReasonJobFailed    = "JobFailed"
44
        ReasonJobScheduled = "JobScheduled"
45
)
46

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

69
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
70
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks/status,verbs=get;update;patch
71
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks/finalizers,verbs=update
72
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
73
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
74
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
75

76
// Reconcile moves the current state of the cluster closer to the desired state
77
func (r *RenderTaskReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
78
        log := ctrl.LoggerFrom(ctx)
647 ✔
79
        ctrlResult := ctrl.Result{}
647 ✔
80

647 ✔
81
        log.V(1).Info("RenderTask is being reconciled", "req", req)
647 ✔
82

647 ✔
83
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
647 ✔
84
                return ctrlResult, nil
51 ✔
85
        }
51 ✔
86

87
        // Fetch the RenderTask instance
88
        res := &solarv1alpha1.RenderTask{}
596 ✔
89
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
596 ✔
90
                if apierrors.IsNotFound(err) {
10 ✔
91
                        return ctrlResult, nil
10 ✔
92
                }
10 ✔
93

94
                return ctrlResult, fmt.Errorf("failed to get RenderTask: %w", err)
×
95
        }
96

97
        // RenderTask instance marked for deletion, stop reconciling
98
        if !res.DeletionTimestamp.IsZero() {
586 ✔
99
                log.V(1).Info("RenderTask is being deleted")
×
100
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "RenderTask is being deleted, cleaning up secret and job")
×
101

×
102
                return ctrlResult, nil
×
103
        }
×
104

105
        // Check if renderjob has already completed successfully
106
        sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobSucceeded)
586 ✔
107
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
586 ✔
108
                log.V(1).Info("RenderTask has already completed successfully, no further action needed")
5 ✔
109

5 ✔
110
                return ctrlResult, nil
5 ✔
111
        }
5 ✔
112

113
        // Determine the namespace for Jobs/Secrets — use the RenderTask's namespace
114
        jobNS := r.taskNamespace(res)
581 ✔
115

581 ✔
116
        // Reconcile Config Secret
117
        configSecret := &corev1.Secret{}
581 ✔
118
        err := r.Get(ctx, r.configSecretKey(res, jobNS), configSecret)
581 ✔
119
        if err != nil && apierrors.IsNotFound(err) {
581 ✔
120
                createdSecret, err := r.createConfigSecret(ctx, res, jobNS)
216 ✔
121
                if err != nil {
216 ✔
122
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateSecretFailed", "CreateConfigSecret", "Failed to create config secret: %s", err)
×
123

×
124
                        return ctrlResult, fmt.Errorf("failed to create secret: %w", err)
×
125
                }
×
126

127
                configSecret = createdSecret
216 ✔
128
        } else if err != nil {
365 ✔
129
                return ctrlResult, fmt.Errorf("failed to get config secret: %w", err)
×
130
        }
×
131

132
        // Resolve push secret from the RenderTask's PushSecretRef
133
        var pushSecret *corev1.Secret
581 ✔
134
        if res.Spec.PushSecretRef != nil {
581 ✔
135
                pushSecret = &corev1.Secret{}
571 ✔
136
                if err := r.Get(ctx, client.ObjectKey{Name: res.Spec.PushSecretRef.Name, Namespace: jobNS}, pushSecret); err != nil {
571 ✔
137
                        return ctrlResult, fmt.Errorf("failed to get push secret: %w", err)
358 ✔
138
                }
358 ✔
139
        }
140

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

151
        // Reconcile Job
152
        job := &batchv1.Job{}
223 ✔
153
        err = r.Get(ctx, r.renderJobKey(res, jobNS), job)
223 ✔
154
        if err != nil && apierrors.IsNotFound(err) {
223 ✔
155
                err := r.createRenderJob(ctx, res, configSecret, pushSecret, sourceSecret, jobNS)
26 ✔
156
                if err != nil {
26 ✔
157
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateJobFailed", "CreateJob", "Failed to create job: %s", err)
×
158

×
159
                        return ctrlResult, fmt.Errorf("failed to create job: %w", err)
×
160
                }
×
161
        } else if err != nil {
197 ✔
162
                return ctrlResult, fmt.Errorf("failed to get job: %w", err)
×
163
        }
×
164

165
        // Update Status
166
        if changed := r.updateResourceStatusFromJob(ctx, res, job); changed {
223 ✔
167
                if err := r.Status().Update(ctx, res); err != nil {
31 ✔
168
                        return ctrlResult, fmt.Errorf("failed to update status: %w", err)
×
169
                }
×
170
        }
171

172
        ttlDuration := time.Duration(ttlSeconds(res.Spec.FailedJobTTL)) * time.Second
223 ✔
173

223 ✔
174
        switch {
223 ✔
175
        case job.Status.Succeeded > 0:
176
                cleanupRenderResources(ctx, r, res, job, jobNS)
3 ✔
177
                log.V(1).Info("Cleaned up after successful job")
3 ✔
178

3 ✔
179
                return ctrlResult, nil
3 ✔
180

181
        case job.Status.Failed > 0:
182
                if shouldCleanupSecrets(res, ttlDuration) {
176 ✔
183
                        cleanupSecrets(ctx, r, res, jobNS)
172 ✔
184
                        log.V(1).Info("Cleaned up secrets after failed job TTL")
172 ✔
185

172 ✔
186
                        return ctrlResult, nil
172 ✔
187
                }
172 ✔
188

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

4 ✔
192
                return ctrl.Result{RequeueAfter: remaining + time.Second}, nil
4 ✔
193
        }
194

195
        return ctrlResult, nil
44 ✔
196
}
197

198
// taskNamespace returns the namespace to use for Jobs/Secrets.
199
func (r *RenderTaskReconciler) taskNamespace(res *solarv1alpha1.RenderTask) string {
200
        return res.Namespace
581 ✔
201
}
581 ✔
202

203
// updateResourceStatusFromJob updates the resource status based on job status
204
func (r *RenderTaskReconciler) updateResourceStatusFromJob(ctx context.Context, res *solarv1alpha1.RenderTask, job *batchv1.Job) (changed bool) {
205
        log := ctrl.LoggerFrom(ctx)
223 ✔
206

223 ✔
207
        if job == nil {
223 ✔
208
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
×
209
                        Type:               ConditionTypeJobScheduled,
×
210
                        Status:             metav1.ConditionFalse,
×
211
                        ObservedGeneration: res.Generation,
×
212
                        Reason:             ReasonDoesNotExist,
×
213
                        Message:            "Renderer job does not exist",
×
214
                })
×
215

×
216
                return changed
×
217
        }
×
218

219
        if job.Status.Succeeded > 0 {
223 ✔
220
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
3 ✔
221
                        Type:               ConditionTypeJobSucceeded,
3 ✔
222
                        Status:             metav1.ConditionTrue,
3 ✔
223
                        ObservedGeneration: res.Generation,
3 ✔
224
                        Reason:             ReasonJobSucceeded,
3 ✔
225
                        Message:            fmt.Sprintf("Renderer job completed successfully at %v", job.Status.CompletionTime),
3 ✔
226
                })
3 ✔
227

3 ✔
228
                chartURL := r.reference(res.Spec.BaseURL, res.Spec.Repository, res.Spec.Tag)
3 ✔
229
                if res.Status.ChartURL != chartURL {
3 ✔
230
                        res.Status.ChartURL = chartURL
3 ✔
231
                        changed = true
3 ✔
232
                }
3 ✔
233

234
                r.Recorder.Eventf(res, job, corev1.EventTypeNormal, ReasonJobSucceeded, "RunJob", "Renderer job completed successfully")
3 ✔
235
                log.V(1).Info("Job succeeded", "name", job.Name)
3 ✔
236

3 ✔
237
                return changed
3 ✔
238
        }
239

240
        if job.Status.Failed > 0 {
220 ✔
241
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
176 ✔
242
                        Type:               ConditionTypeJobFailed,
176 ✔
243
                        Status:             metav1.ConditionTrue,
176 ✔
244
                        ObservedGeneration: res.Generation,
176 ✔
245
                        Reason:             ReasonJobFailed,
176 ✔
246
                        Message:            "Renderer job failed",
176 ✔
247
                })
176 ✔
248
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, ReasonJobFailed, "RunJob", "Renderer job failed")
176 ✔
249
                log.V(1).Info("Job failed", "name", job.Name)
176 ✔
250

176 ✔
251
                return changed
176 ✔
252
        }
176 ✔
253

254
        return apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
44 ✔
255
                Type:               ConditionTypeJobScheduled,
44 ✔
256
                Status:             metav1.ConditionTrue,
44 ✔
257
                ObservedGeneration: res.Generation,
44 ✔
258
                Reason:             ReasonJobScheduled,
44 ✔
259
                Message:            fmt.Sprintf("Renderer job is running (active: %d, succeeded: %d, failed: %d)", job.Status.Active, job.Status.Succeeded, job.Status.Failed),
44 ✔
260
        })
44 ✔
261
}
262

263
func (r *RenderTaskReconciler) deleteRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) error {
264
        job := &batchv1.Job{}
3 ✔
265
        if err := r.Get(ctx, r.renderJobKey(res, jobNS), job); err != nil {
3 ✔
266
                return err
×
267
        }
×
268

269
        return r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
3 ✔
270
}
271

272
func (r *RenderTaskReconciler) deleteConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) error {
273
        secret := &corev1.Secret{}
175 ✔
274
        if err := r.Get(ctx, r.configSecretKey(res, jobNS), secret); err != nil {
175 ✔
275
                return err
10 ✔
276
        }
10 ✔
277

278
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
165 ✔
279
}
280

281
func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, configSecret, pushSecret, sourceSecret *corev1.Secret, jobNS string) error {
282
        jobKey := r.renderJobKey(res, jobNS)
26 ✔
283
        jobName := jobKey.Name
26 ✔
284
        backoffLimit := int32(3)
26 ✔
285
        ttlSecondsAfterFinished := int32(3600)
26 ✔
286
        if res.Spec.FailedJobTTL != nil {
26 ✔
287
                ttlSecondsAfterFinished = *res.Spec.FailedJobTTL
2 ✔
288
        }
2 ✔
289

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

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

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

26 ✔
357
        args := slices.Clone(r.RendererArgs)
26 ✔
358
        args = append(args, "/etc/renderer/config.json", fmt.Sprintf("--url=%s", pushURL))
26 ✔
359
        if res.Spec.PlainHTTP {
26 ✔
360
                args = append(args, "--plain-http=true")
×
361
        }
×
362

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

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

415
                case corev1.SecretTypeDockerConfigJson:
416
                        job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
1 ✔
417
                                Name: "dockerconfig",
1 ✔
418
                                Secret: &corev1.SecretVolumeSource{
1 ✔
419
                                        SecretName: pushSecret.Name,
1 ✔
420
                                        Items: []corev1.KeyToPath{
1 ✔
421
                                                {
1 ✔
422
                                                        Key:  ".dockerconfigjson",
1 ✔
423
                                                        Path: "dockerconfig.json",
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
        // Credentials for reading the OCM component. Kept separate from the push
445
        // credentials because the source registry is frequently a different one.
446
        switch {
26 ✔
447
        case hasBasicAuthKeys(sourceSecret):
448
                job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env,
2 ✔
449
                        corev1.EnvVar{
2 ✔
450
                                Name: "SOURCE_REGISTRY_USERNAME",
2 ✔
451
                                ValueFrom: &corev1.EnvVarSource{
2 ✔
452
                                        SecretKeyRef: &corev1.SecretKeySelector{
2 ✔
453
                                                Name: sourceSecret.Name,
2 ✔
454
                                                Key:  secretKeyUsername,
2 ✔
455
                                        },
2 ✔
456
                                },
2 ✔
457
                        },
2 ✔
458
                        corev1.EnvVar{
2 ✔
459
                                Name: "SOURCE_REGISTRY_PASSWORD",
2 ✔
460
                                ValueFrom: &corev1.EnvVarSource{
2 ✔
461
                                        SecretKeyRef: &corev1.SecretKeySelector{
2 ✔
462
                                                Name: sourceSecret.Name,
2 ✔
463
                                                Key:  secretKeyPassword,
2 ✔
464
                                        },
2 ✔
465
                                },
2 ✔
466
                        },
2 ✔
467
                )
2 ✔
468

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

1 ✔
485
                job.Spec.Template.Spec.Containers[0].VolumeMounts = append(
1 ✔
486
                        job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
1 ✔
487
                                Name:      "source-dockerconfig",
1 ✔
488
                                MountPath: sourceDockerConfigPath,
1 ✔
489
                                SubPath:   "config.json",
1 ✔
490
                                ReadOnly:  true,
1 ✔
491
                        })
1 ✔
492

1 ✔
493
                job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env,
1 ✔
494
                        corev1.EnvVar{
1 ✔
495
                                Name:  "SOURCE_DOCKER_CONFIG",
1 ✔
496
                                Value: sourceDockerConfigPath,
1 ✔
497
                        })
1 ✔
498
        }
499

500
        if len(r.RendererImagePullSecrets) > 0 {
26 ✔
501
                refs := make([]corev1.LocalObjectReference, len(r.RendererImagePullSecrets))
1 ✔
502
                for i, n := range r.RendererImagePullSecrets {
1 ✔
503
                        refs[i] = corev1.LocalObjectReference{Name: n}
2 ✔
504
                }
2 ✔
505
                job.Spec.Template.Spec.ImagePullSecrets = refs
1 ✔
506
        }
507

508
        // Set owner references
509
        if err := controllerutil.SetControllerReference(res, job, r.Scheme); err != nil {
26 ✔
510
                return fmt.Errorf("failed to set controller reference on Job: %w", err)
×
511
        }
×
512

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

×
516
                return err
×
517
        }
×
518

519
        res.Status.JobRef = &corev1.ObjectReference{
26 ✔
520
                APIVersion: batchv1.SchemeGroupVersion.String(),
26 ✔
521
                Kind:       "Job",
26 ✔
522
                Namespace:  job.Namespace,
26 ✔
523
                Name:       job.Name,
26 ✔
524
        }
26 ✔
525

526
        if err := r.Status().Update(ctx, res); err != nil {
26 ✔
527
                return fmt.Errorf("failed to update status: %w", err)
×
528
        }
×
529

530
        return nil
26 ✔
531
}
532

533
// secretKeyUsername and secretKeyPassword are the keys SolAr reads from a
534
// Registry's solarSecretRef, mirroring pkg/discovery/registry_provider.go.
535
const (
536
        secretKeyUsername = "username"
537
        secretKeyPassword = "password"
538

539
        // sourceDockerConfigPath is where a dockerconfigjson source secret is
540
        // mounted in the render Pod, kept distinct from the push secret's mount.
541
        sourceDockerConfigPath = "/etc/renderer/source-dockerconfig.json"
542
)
543

544
// hasBasicAuthKeys reports whether secret carries both credential keys with
545
// non-empty values, regardless of its declared Secret type. Empty values are
546
// rejected
547
func hasBasicAuthKeys(secret *corev1.Secret) bool {
548
        if secret == nil {
26 ✔
549
                return false
19 ✔
550
        }
19 ✔
551

552
        username, hasUser := secret.Data[secretKeyUsername]
7 ✔
553
        password, hasPass := secret.Data[secretKeyPassword]
7 ✔
554

7 ✔
555
        return hasUser && hasPass && len(username) > 0 && len(password) > 0
6 ✔
556
}
557

558
// hasDockerConfigJSON reports whether secret carries a non-empty docker config,
559
// the other shape a Registry's solarSecretRef can take.
560
func hasDockerConfigJSON(secret *corev1.Secret) bool {
561
        if secret == nil {
24 ✔
562
                return false
19 ✔
563
        }
19 ✔
564

565
        config, ok := secret.Data[corev1.DockerConfigJsonKey]
5 ✔
566

5 ✔
567
        return ok && len(config) > 0
5 ✔
568
}
569

570
func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) (*corev1.Secret, error) {
571
        cfgJson, err := json.Marshal(res.Spec.RendererConfig)
216 ✔
572
        if err != nil {
216 ✔
573
                return nil, err
×
574
        }
×
575

576
        secretKey := r.configSecretKey(res, jobNS)
216 ✔
577
        secret := &corev1.Secret{
216 ✔
578
                Name:      secretKey.Name,
216 ✔
579
                Namespace: secretKey.Namespace,
216 ✔
580
                Annotations: map[string]string{
216 ✔
581
                        annotationSecretName: secretKey.Name,
216 ✔
582
                },
216 ✔
583
                Type: corev1.SecretTypeOpaque,
216 ✔
584
                Data: map[string][]byte{
216 ✔
585
                        "config.json": cfgJson,
216 ✔
586
                },
216 ✔
587
        }
216 ✔
588

589
        // Set owner references
590
        if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil {
216 ✔
591
                return nil, fmt.Errorf("failed to set controller reference on Secret: %w", err)
×
592
        }
×
593

594
        if err := r.Create(ctx, secret); err != nil {
216 ✔
595
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
×
596

×
597
                return nil, err
×
598
        }
×
599

600
        res.Status.ConfigSecretRef = &corev1.ObjectReference{
216 ✔
601
                APIVersion: corev1.SchemeGroupVersion.String(),
216 ✔
602
                Kind:       "Secret",
216 ✔
603
                Namespace:  secret.Namespace,
216 ✔
604
                Name:       secret.Name,
216 ✔
605
        }
216 ✔
606

607
        if err := r.Status().Update(ctx, res); err != nil {
216 ✔
608
                return nil, fmt.Errorf("failed to update status: %w", err)
×
609
        }
×
610

611
        return secret, nil
216 ✔
612
}
613

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

621
func (r *RenderTaskReconciler) renderJobKey(res *solarv1alpha1.RenderTask, jobNS string) client.ObjectKey {
622
        return client.ObjectKey{
252 ✔
623
                Name:      truncateName(fmt.Sprintf("render-%s", res.Name), maxK8sLabelValueLen),
252 ✔
624
                Namespace: jobNS,
252 ✔
625
        }
252 ✔
626
}
627

628
func (r *RenderTaskReconciler) reference(baseURL, repo, tag string) string {
629
        base := baseURL
29 ✔
630
        if !strings.HasPrefix(base, "oci://") {
29 ✔
631
                base = fmt.Sprintf("oci://%s", base)
19 ✔
632
        }
19 ✔
633

634
        base = strings.TrimSuffix(base, "/")
29 ✔
635

29 ✔
636
        return fmt.Sprintf("%s/%s:%s", base, repo, tag)
29 ✔
637
}
638

639
func ttlSeconds(ttl *int32) int32 {
640
        if ttl != nil {
223 ✔
641
                return *ttl
178 ✔
642
        }
178 ✔
643

644
        return 3600
45 ✔
645
}
646

647
func shouldCleanupSecrets(res *solarv1alpha1.RenderTask, ttl time.Duration) bool {
648
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
176 ✔
649

176 ✔
650
        return cond != nil && time.Since(cond.LastTransitionTime.Time) >= ttl
176 ✔
651
}
176 ✔
652

653
func remainingTTL(res *solarv1alpha1.RenderTask, ttl time.Duration) time.Duration {
654
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
4 ✔
655
        if cond == nil {
4 ✔
656
                return ttl
×
657
        }
×
658

659
        remaining := ttl - time.Since(cond.LastTransitionTime.Time)
4 ✔
660
        if remaining < 0 {
4 ✔
661
                return 0
×
662
        }
×
663

664
        return remaining
4 ✔
665
}
666

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

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

680
// SetupWithManager sets up the controller with the Manager.
681
func (r *RenderTaskReconciler) SetupWithManager(mgr ctrl.Manager) error {
682
        return ctrl.NewControllerManagedBy(mgr).
1 ✔
683
                For(&solarv1alpha1.RenderTask{}).
1 ✔
684
                Owns(&batchv1.Job{}).
1 ✔
685
                Owns(&corev1.Secret{}).
1 ✔
686
                Complete(r)
1 ✔
687
}
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