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

opendefensecloud / solution-arsenal / 39598200574

21 Oct 2026 12:15PM UTC coverage: 79.557% (+0.1%) from 79.424%
39598200574

push

github

web-flow
refactor(controller): make condition reasons constants (#885)

## What
Condition reasons in the controllers are now `Reason*` constants next to
the `ConditionType*` ones. Controllers and unit tests use them, string
values are unchanged.

## Why
Reasons are part of the status contract -> a typo or rename in a literal
silently breaks consumers. `NotGranted`, `NotFound` and `Resolved` are
shared by the Release and Target reconcilers, so they live once in
`release_controller.go`.

e2e keeps the literals on purpose, its a black-box check of the wire
values.

## Testing
Pure refactor, no behavior change.

```
$ go build ./... && go vet ./pkg/controller/
$ go test ./pkg/controller/
ok  	go.opendefense.cloud/solar/pkg/controller	198.147s
```

## Checklist
- [x] Tests added/updated (assertions switched to the constants)
- [x] No breaking changes
- [x] Readable commit history (squashed and cleaned up as desired)
- [x] AI code review considered and comments resolved

Closes #860

31 of 38 new or added lines in 5 files covered. (81.58%)

1 existing line in 1 file now uncovered.

5390 of 6775 relevant lines covered (79.56%)

34.29 hits per line

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

87.07
/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)
622 ✔
79
        ctrlResult := ctrl.Result{}
622 ✔
80

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

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

87
        // Fetch the RenderTask instance
88
        res := &solarv1alpha1.RenderTask{}
571 ✔
89
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
571 ✔
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() {
561 ✔
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)
561 ✔
107
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
561 ✔
108
                log.V(1).Info("RenderTask has already completed successfully, no further action needed")
6 ✔
109

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

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

555 ✔
116
        // Reconcile Config Secret
117
        configSecret := &corev1.Secret{}
555 ✔
118
        err := r.Get(ctx, r.configSecretKey(res, jobNS), configSecret)
555 ✔
119
        if err != nil && apierrors.IsNotFound(err) {
555 ✔
120
                createdSecret, err := r.createConfigSecret(ctx, res, jobNS)
200 ✔
121
                if err != nil {
200 ✔
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
200 ✔
128
        } else if err != nil {
355 ✔
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
555 ✔
134
        if res.Spec.PushSecretRef != nil {
555 ✔
135
                pushSecret = &corev1.Secret{}
545 ✔
136
                if err := r.Get(ctx, client.ObjectKey{Name: res.Spec.PushSecretRef.Name, Namespace: jobNS}, pushSecret); err != nil {
545 ✔
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
197 ✔
144
        if res.Spec.SourceSecretRef != nil {
197 ✔
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{}
197 ✔
153
        err = r.Get(ctx, r.renderJobKey(res, jobNS), job)
197 ✔
154
        if err != nil && apierrors.IsNotFound(err) {
197 ✔
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 {
171 ✔
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 {
197 ✔
167
                if err := r.Status().Update(ctx, res); err != nil {
32 ✔
168
                        return ctrlResult, fmt.Errorf("failed to update status: %w", err)
1 ✔
169
                }
1 ✔
170
        }
171

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

196 ✔
174
        switch {
196 ✔
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) {
150 ✔
183
                        cleanupSecrets(ctx, r, res, jobNS)
146 ✔
184
                        log.V(1).Info("Cleaned up secrets after failed job TTL")
146 ✔
185

146 ✔
186
                        return ctrlResult, nil
146 ✔
187
                }
146 ✔
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
43 ✔
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
555 ✔
201
}
555 ✔
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)
197 ✔
206

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

×
216
                return changed
×
217
        }
×
218

219
        if job.Status.Succeeded > 0 {
197 ✔
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 {
194 ✔
241
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
150 ✔
242
                        Type:               ConditionTypeJobFailed,
150 ✔
243
                        Status:             metav1.ConditionTrue,
150 ✔
244
                        ObservedGeneration: res.Generation,
150 ✔
245
                        Reason:             ReasonJobFailed,
150 ✔
246
                        Message:            "Renderer job failed",
150 ✔
247
                })
150 ✔
248
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, ReasonJobFailed, "RunJob", "Renderer job failed")
150 ✔
249
                log.V(1).Info("Job failed", "name", job.Name)
150 ✔
250

150 ✔
251
                return changed
150 ✔
252
        }
150 ✔
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{}
149 ✔
274
        if err := r.Get(ctx, r.configSecretKey(res, jobNS), secret); err != nil {
149 ✔
275
                return err
5 ✔
276
        }
5 ✔
277

278
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
144 ✔
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{
26 ✔
305
                {
26 ✔
306
                        Name:      "config",
26 ✔
307
                        MountPath: "/etc/renderer/config.json",
26 ✔
308
                        SubPath:   "config.json",
26 ✔
309
                        ReadOnly:  true,
26 ✔
310
                },
26 ✔
311
        }
26 ✔
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
7 ✔
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)
200 ✔
572
        if err != nil {
200 ✔
573
                return nil, err
×
574
        }
×
575

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

589
        // Set owner references
590
        if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil {
200 ✔
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 {
200 ✔
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{
200 ✔
601
                APIVersion: corev1.SchemeGroupVersion.String(),
200 ✔
602
                Kind:       "Secret",
200 ✔
603
                Namespace:  secret.Namespace,
200 ✔
604
                Name:       secret.Name,
200 ✔
605
        }
200 ✔
606

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

611
        return secret, nil
200 ✔
612
}
613

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

621
func (r *RenderTaskReconciler) renderJobKey(res *solarv1alpha1.RenderTask, jobNS string) client.ObjectKey {
622
        return client.ObjectKey{
226 ✔
623
                Name:      truncateName(fmt.Sprintf("render-%s", res.Name), maxK8sLabelValueLen),
226 ✔
624
                Namespace: jobNS,
226 ✔
625
        }
226 ✔
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 {
196 ✔
641
                return *ttl
152 ✔
642
        }
152 ✔
643

644
        return 3600
44 ✔
645
}
646

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

150 ✔
650
        return cond != nil && time.Since(cond.LastTransitionTime.Time) >= ttl
150 ✔
651
}
150 ✔
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) {
149 ✔
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