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

opendefensecloud / solution-arsenal / 31585127502

12 Aug 2026 09:55AM UTC coverage: 80.3% (-1.8%) from 82.081%
31585127502

push

github

web-flow
feat: pull secrets provision added for deployments to solar via ocm-kit (#739)

## What
Moves helm values-template rendering out of discovery and into
`solar-renderer`, so
the rendered values are computed per-Target and can carry that target's
registry pull
secrets. Charts can now emit `imagePullSecrets` on the workloads they
create, without
the user setting any `Release.spec.values`.
Closes #560 

## Why
FluxCD resources have their pull secrets
(`OCIRepository.spec.secretRef`), but the workloads
the chart itself creates came out with no `imagePullSecrets` if not
explicitly suplpied by
the user. Those Deployments failed to pull from private registries.

## Testing
- **Unit:** `pkg/renderer/values_template_test.go` (template rendering,
pull-secret
merging, credential/consumer-identity setup),
`pkg/ociregistry/host_test.go`,
`api/solar/v1alpha1/component_types_test.go` (`OCMRef` incl. empty-name
degradation).
- **envtest:** `pkg/controller/rendertask_sourcecreds_test.go` covers
both source-secret
shapes landing correctly on the render Job; `target_controller_test.go`
extended for
`ref`/`pullSecrets`/`sourceSecretRef` propagation and tag-drift on
binding changes.
- **e2e:** new `test/e2e` contexts "helm values templating" and "pull
secrets for chart
workloads" — the latter builds a CTF from
`test/fixtures/pullsecret-demo`, transfers it
into the in-cluster registry, creates a Release with **no** values, and
asserts the
pull secret appears both in the rendered ConfigMap and on the resulting
Deployment.
Last full green run was against the pre-`third_party`-removal tree;
worth one more
  `make test-e2e` before merge.

## Notes for reviewers

- `HelmResourceMetadata.valuesTemplate` is gone from `ComponentVersion`.

 - `ComponentSpec.name` is new and only written by discovery. Components
discovered before this change have an empty `name`, so `OCMRef` returns
`""` and the
renderer silently skips values-template rendering. A re-scan repopulates
the field.


## C... (continued)

207 of 275 new or added lines in 10 files covered. (75.27%)

6 existing lines in 3 files now uncovered.

5466 of 6807 relevant lines covered (80.3%)

32.69 hits per line

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

87.81
/pkg/controller/rendertask_controller.go
1
// Copyright 2026 BWI GmbH and Solution Arsenal contributors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package controller
5

6
import (
7
        "context"
8
        "encoding/json"
9
        "fmt"
10
        "slices"
11
        "strings"
12
        "time"
13

14
        batchv1 "k8s.io/api/batch/v1"
15
        corev1 "k8s.io/api/core/v1"
16
        apierrors "k8s.io/apimachinery/pkg/api/errors"
17
        apimeta "k8s.io/apimachinery/pkg/api/meta"
18
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19
        "k8s.io/apimachinery/pkg/runtime"
20
        "k8s.io/client-go/tools/events"
21
        ctrl "sigs.k8s.io/controller-runtime"
22
        "sigs.k8s.io/controller-runtime/pkg/client"
23
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
24

25
        solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1"
26
)
27

28
const (
29
        annotationJobName    = "solar.opendefense.cloud/job-name"
30
        annotationSecretName = "solar.opendefense.cloud/secret-name"
31

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

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

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

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

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

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

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

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

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

91
        // RenderTask instance marked for deletion, stop reconciling
92
        if !res.DeletionTimestamp.IsZero() {
572✔
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)
572✔
101
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
576✔
102
                log.V(1).Info("RenderTask has already completed successfully, no further action needed")
4✔
103

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

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

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

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

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

126
        // Resolve push secret from the RenderTask's PushSecretRef
127
        var pushSecret *corev1.Secret
568✔
128
        if res.Spec.PushSecretRef != nil {
1,126✔
129
                pushSecret = &corev1.Secret{}
558✔
130
                if err := r.Get(ctx, client.ObjectKey{Name: res.Spec.PushSecretRef.Name, Namespace: jobNS}, pushSecret); err != nil {
786✔
131
                        return ctrlResult, errLogAndWrap(log, err, "failed to get push secret")
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
340✔
138
        if res.Spec.SourceSecretRef != nil {
347✔
139
                sourceSecret = &corev1.Secret{}
7✔
140
                if err := r.Get(ctx, client.ObjectKey{Name: res.Spec.SourceSecretRef.Name, Namespace: jobNS}, sourceSecret); err != nil {
7✔
NEW
141
                        return ctrlResult, errLogAndWrap(log, err, "failed to get source secret")
×
NEW
142
                }
×
143
        }
144

145
        // Reconcile Job
146
        job := &batchv1.Job{}
340✔
147
        err = r.Get(ctx, r.renderJobKey(res, jobNS), job)
340✔
148
        if err != nil && apierrors.IsNotFound(err) {
366✔
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, errLogAndWrap(log, err, "failed to create job")
×
154
                }
×
155
        } else if err != nil {
314✔
156
                return ctrlResult, errLogAndWrap(log, err, "could not get job")
×
157
        }
×
158

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

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

340✔
168
        switch {
340✔
169
        case job.Status.Succeeded > 0:
3✔
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:
294✔
176
                if shouldCleanupSecrets(res, ttlDuration) {
584✔
177
                        cleanupSecrets(ctx, r, res, jobNS)
290✔
178
                        log.V(1).Info("Cleaned up secrets after failed job TTL")
290✔
179

290✔
180
                        return ctrlResult, nil
290✔
181
                }
290✔
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
43✔
190
}
191

192
// taskNamespace returns the namespace to use for Jobs/Secrets.
193
func (r *RenderTaskReconciler) taskNamespace(res *solarv1alpha1.RenderTask) string {
568✔
194
        return res.Namespace
568✔
195
}
568✔
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) {
340✔
199
        log := ctrl.LoggerFrom(ctx)
340✔
200

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

294✔
245
                return changed
294✔
246
        }
294✔
247

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

257
func (r *RenderTaskReconciler) deleteRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) error {
3✔
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 {
293✔
267
        secret := &corev1.Secret{}
293✔
268
        if err := r.Get(ctx, r.configSecretKey(res, jobNS), secret); err != nil {
293✔
UNCOV
269
                return err
×
UNCOV
270
        }
×
271

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

275
func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, configSecret, pushSecret, sourceSecret *corev1.Secret, jobNS string) error {
26✔
276
        log := ctrl.LoggerFrom(ctx)
26✔
277

26✔
278
        jobKey := r.renderJobKey(res, jobNS)
26✔
279
        jobName := jobKey.Name
26✔
280
        backoffLimit := int32(3)
26✔
281
        ttlSecondsAfterFinished := int32(3600)
26✔
282
        if res.Spec.FailedJobTTL != nil {
28✔
283
                ttlSecondsAfterFinished = *res.Spec.FailedJobTTL
2✔
284
        }
2✔
285

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

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

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

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

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

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

423
                case corev1.SecretTypeDockerConfigJson:
1✔
424
                        job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
1✔
425
                                Name: "dockerconfig",
1✔
426
                                VolumeSource: corev1.VolumeSource{
1✔
427
                                        Secret: &corev1.SecretVolumeSource{
1✔
428
                                                SecretName: pushSecret.Name,
1✔
429
                                                Items: []corev1.KeyToPath{
1✔
430
                                                        {
1✔
431
                                                                Key:  ".dockerconfigjson",
1✔
432
                                                                Path: "dockerconfig.json",
1✔
433
                                                        },
1✔
434
                                                },
1✔
435
                                        },
1✔
436
                                },
1✔
437
                        })
1✔
438

1✔
439
                        job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
1✔
440
                                Name:      "dockerconfig",
1✔
441
                                MountPath: "/etc/renderer/dockerconfig.json",
1✔
442
                                SubPath:   "dockerconfig.json",
1✔
443
                                ReadOnly:  true,
1✔
444
                        })
1✔
445

1✔
446
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{
1✔
447
                                Name:  "DOCKER_CONFIG",
1✔
448
                                Value: "/etc/renderer/dockerconfig.json",
1✔
449
                        })
1✔
450
                default:
14✔
451
                }
452
        }
453

454
        // Credentials for reading the OCM component. Kept separate from the push
455
        // credentials because the source registry is frequently a different one.
456
        switch {
26✔
457
        case hasBasicAuthKeys(sourceSecret):
2✔
458
                job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env,
2✔
459
                        corev1.EnvVar{
2✔
460
                                Name: "SOURCE_REGISTRY_USERNAME",
2✔
461
                                ValueFrom: &corev1.EnvVarSource{
2✔
462
                                        SecretKeyRef: &corev1.SecretKeySelector{
2✔
463
                                                LocalObjectReference: corev1.LocalObjectReference{
2✔
464
                                                        Name: sourceSecret.Name,
2✔
465
                                                },
2✔
466
                                                Key: secretKeyUsername,
2✔
467
                                        },
2✔
468
                                },
2✔
469
                        },
2✔
470
                        corev1.EnvVar{
2✔
471
                                Name: "SOURCE_REGISTRY_PASSWORD",
2✔
472
                                ValueFrom: &corev1.EnvVarSource{
2✔
473
                                        SecretKeyRef: &corev1.SecretKeySelector{
2✔
474
                                                LocalObjectReference: corev1.LocalObjectReference{
2✔
475
                                                        Name: sourceSecret.Name,
2✔
476
                                                },
2✔
477
                                                Key: secretKeyPassword,
2✔
478
                                        },
2✔
479
                                },
2✔
480
                        },
2✔
481
                )
2✔
482

483
        case hasDockerConfigJSON(sourceSecret):
1✔
484
                // Mounted at its own path so it cannot collide with the push secret's
1✔
485
                // docker config
1✔
486
                job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
1✔
487
                        Name: "source-dockerconfig",
1✔
488
                        VolumeSource: corev1.VolumeSource{
1✔
489
                                Secret: &corev1.SecretVolumeSource{
1✔
490
                                        SecretName: sourceSecret.Name,
1✔
491
                                        Items: []corev1.KeyToPath{
1✔
492
                                                {
1✔
493
                                                        Key:  corev1.DockerConfigJsonKey,
1✔
494
                                                        Path: "config.json",
1✔
495
                                                },
1✔
496
                                        },
1✔
497
                                },
1✔
498
                        },
1✔
499
                })
1✔
500

1✔
501
                job.Spec.Template.Spec.Containers[0].VolumeMounts = append(
1✔
502
                        job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
1✔
503
                                Name:      "source-dockerconfig",
1✔
504
                                MountPath: sourceDockerConfigPath,
1✔
505
                                SubPath:   "config.json",
1✔
506
                                ReadOnly:  true,
1✔
507
                        })
1✔
508

1✔
509
                job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env,
1✔
510
                        corev1.EnvVar{
1✔
511
                                Name:  "SOURCE_DOCKER_CONFIG",
1✔
512
                                Value: sourceDockerConfigPath,
1✔
513
                        })
1✔
514
        }
515

516
        if len(r.RendererImagePullSecrets) > 0 {
27✔
517
                refs := make([]corev1.LocalObjectReference, len(r.RendererImagePullSecrets))
1✔
518
                for i, n := range r.RendererImagePullSecrets {
3✔
519
                        refs[i] = corev1.LocalObjectReference{Name: n}
2✔
520
                }
2✔
521
                job.Spec.Template.Spec.ImagePullSecrets = refs
1✔
522
        }
523

524
        // Set owner references
525
        if err := controllerutil.SetControllerReference(res, job, r.Scheme); err != nil {
26✔
526
                return errLogAndWrap(log, err, "failed to set controller reference")
×
527
        }
×
528

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

×
532
                return errLogAndWrap(log, err, "job creation failed")
×
533
        }
×
534

535
        res.Status.JobRef = &corev1.ObjectReference{
26✔
536
                APIVersion: batchv1.SchemeGroupVersion.String(),
26✔
537
                Kind:       "Job",
26✔
538
                Namespace:  job.Namespace,
26✔
539
                Name:       job.Name,
26✔
540
        }
26✔
541

26✔
542
        if err := r.Status().Update(ctx, res); err != nil {
26✔
543
                return errLogAndWrap(log, err, "failed to update status")
×
544
        }
×
545

546
        return nil
26✔
547
}
548

549
// secretKeyUsername and secretKeyPassword are the keys SolAr reads from a
550
// Registry's solarSecretRef, mirroring pkg/discovery/registry_provider.go.
551
const (
552
        secretKeyUsername = "username"
553
        secretKeyPassword = "password"
554

555
        // sourceDockerConfigPath is where a dockerconfigjson source secret is
556
        // mounted in the render Pod, kept distinct from the push secret's mount.
557
        sourceDockerConfigPath = "/etc/renderer/source-dockerconfig.json"
558
)
559

560
// hasBasicAuthKeys reports whether secret carries both credential keys with
561
// non-empty values, regardless of its declared Secret type. Empty values are
562
// rejected
563
func hasBasicAuthKeys(secret *corev1.Secret) bool {
26✔
564
        if secret == nil {
45✔
565
                return false
19✔
566
        }
19✔
567

568
        username, hasUser := secret.Data[secretKeyUsername]
7✔
569
        password, hasPass := secret.Data[secretKeyPassword]
7✔
570

7✔
571
        return hasUser && hasPass && len(username) > 0 && len(password) > 0
7✔
572
}
573

574
// hasDockerConfigJSON reports whether secret carries a non-empty docker config,
575
// the other shape a Registry's solarSecretRef can take.
576
func hasDockerConfigJSON(secret *corev1.Secret) bool {
24✔
577
        if secret == nil {
43✔
578
                return false
19✔
579
        }
19✔
580

581
        config, ok := secret.Data[corev1.DockerConfigJsonKey]
5✔
582

5✔
583
        return ok && len(config) > 0
5✔
584
}
585

586
func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) (*corev1.Secret, error) {
336✔
587
        log := ctrl.LoggerFrom(ctx)
336✔
588

336✔
589
        cfgJson, err := json.Marshal(res.Spec.RendererConfig)
336✔
590
        if err != nil {
336✔
591
                return nil, err
×
592
        }
×
593

594
        secretKey := r.configSecretKey(res, jobNS)
336✔
595
        secret := &corev1.Secret{
336✔
596
                ObjectMeta: metav1.ObjectMeta{
336✔
597
                        Name:      secretKey.Name,
336✔
598
                        Namespace: secretKey.Namespace,
336✔
599
                        Annotations: map[string]string{
336✔
600
                                annotationSecretName: secretKey.Name,
336✔
601
                        },
336✔
602
                },
336✔
603
                Type: corev1.SecretTypeOpaque,
336✔
604
                Data: map[string][]byte{
336✔
605
                        "config.json": cfgJson,
336✔
606
                },
336✔
607
        }
336✔
608

336✔
609
        // Set owner references
336✔
610
        if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil {
336✔
611
                return nil, errLogAndWrap(log, err, "failed to set controller reference")
×
612
        }
×
613

614
        if err := r.Create(ctx, secret); err != nil {
336✔
615
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
×
616

×
617
                return nil, errLogAndWrap(log, err, "secret creation failed")
×
618
        }
×
619

620
        res.Status.ConfigSecretRef = &corev1.ObjectReference{
336✔
621
                APIVersion: corev1.SchemeGroupVersion.String(),
336✔
622
                Kind:       "Secret",
336✔
623
                Namespace:  secret.Namespace,
336✔
624
                Name:       secret.Name,
336✔
625
        }
336✔
626

336✔
627
        if err := r.Status().Update(ctx, res); err != nil {
336✔
628
                return nil, errLogAndWrap(log, err, "failed to update status")
×
629
        }
×
630

631
        return secret, nil
336✔
632
}
633

634
func (r *RenderTaskReconciler) configSecretKey(res *solarv1alpha1.RenderTask, jobNS string) client.ObjectKey {
1,197✔
635
        return client.ObjectKey{
1,197✔
636
                Name:      truncateName(fmt.Sprintf("render-%s", res.Name), maxK8sLabelValueLen),
1,197✔
637
                Namespace: jobNS,
1,197✔
638
        }
1,197✔
639
}
1,197✔
640

641
func (r *RenderTaskReconciler) renderJobKey(res *solarv1alpha1.RenderTask, jobNS string) client.ObjectKey {
369✔
642
        return client.ObjectKey{
369✔
643
                Name:      truncateName(fmt.Sprintf("render-%s", res.Name), maxK8sLabelValueLen),
369✔
644
                Namespace: jobNS,
369✔
645
        }
369✔
646
}
369✔
647

648
func (r *RenderTaskReconciler) reference(baseURL, repo, tag string) string {
29✔
649
        base := baseURL
29✔
650
        if !strings.HasPrefix(base, "oci://") {
48✔
651
                base = fmt.Sprintf("oci://%s", base)
19✔
652
        }
19✔
653

654
        base = strings.TrimSuffix(base, "/")
29✔
655

29✔
656
        return fmt.Sprintf("%s/%s:%s", base, repo, tag)
29✔
657
}
658

659
func ttlSeconds(ttl *int32) int32 {
340✔
660
        if ttl != nil {
636✔
661
                return *ttl
296✔
662
        }
296✔
663

664
        return 3600
44✔
665
}
666

667
func shouldCleanupSecrets(res *solarv1alpha1.RenderTask, ttl time.Duration) bool {
294✔
668
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
294✔
669

294✔
670
        return cond != nil && time.Since(cond.LastTransitionTime.Time) >= ttl
294✔
671
}
294✔
672

673
func remainingTTL(res *solarv1alpha1.RenderTask, ttl time.Duration) time.Duration {
4✔
674
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
4✔
675
        if cond == nil {
4✔
676
                return ttl
×
677
        }
×
678

679
        remaining := ttl - time.Since(cond.LastTransitionTime.Time)
4✔
680
        if remaining < 0 {
4✔
681
                return 0
×
682
        }
×
683

684
        return remaining
4✔
685
}
686

687
func cleanupSecrets(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, jobNS string) {
293✔
688
        if err := r.deleteConfigSecret(ctx, res, jobNS); err != nil && !apierrors.IsNotFound(err) {
293✔
689
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete config secret: %s", err)
×
690
        }
×
691
}
692

693
func cleanupRenderResources(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, job *batchv1.Job, jobNS string) {
3✔
694
        cleanupSecrets(ctx, r, res, jobNS)
3✔
695
        if err := r.deleteRenderJob(ctx, res, jobNS); err != nil && !apierrors.IsNotFound(err) {
3✔
696
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete job: %s", err)
×
697
        }
×
698
}
699

700
// SetupWithManager sets up the controller with the Manager.
701
func (r *RenderTaskReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
702
        return ctrl.NewControllerManagedBy(mgr).
1✔
703
                For(&solarv1alpha1.RenderTask{}).
1✔
704
                Owns(&batchv1.Job{}).
1✔
705
                Owns(&corev1.Secret{}).
1✔
706
                Complete(r)
1✔
707
}
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