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

opendefensecloud / solution-arsenal / 30884675117

04 Aug 2026 06:37AM UTC coverage: 82.075% (+0.05%) from 82.027%
30884675117

Pull #730

github

web-flow
Merge 360354562 into fa9581050
Pull Request #730: fix: ignore terminating renderartifacts

49 of 54 new or added lines in 2 files covered. (90.74%)

8 existing lines in 3 files now uncovered.

5293 of 6449 relevant lines covered (82.07%)

32.29 hits per line

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

80.0
/pkg/controller/renderartifact_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
        "errors"
10
        "fmt"
11
        "net/http"
12
        "slices"
13
        "strings"
14
        "time"
15

16
        "github.com/google/go-containerregistry/pkg/authn"
17
        "github.com/google/go-containerregistry/pkg/v1/remote/transport"
18
        corev1 "k8s.io/api/core/v1"
19
        apierrors "k8s.io/apimachinery/pkg/api/errors"
20
        apimeta "k8s.io/apimachinery/pkg/api/meta"
21
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22
        "k8s.io/apimachinery/pkg/runtime"
23
        "k8s.io/apimachinery/pkg/types"
24
        "k8s.io/client-go/tools/events"
25
        ctrl "sigs.k8s.io/controller-runtime"
26
        "sigs.k8s.io/controller-runtime/pkg/client"
27
        "sigs.k8s.io/controller-runtime/pkg/handler"
28
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
29

30
        solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1"
31
        "go.opendefense.cloud/solar/pkg/ociregistry"
32
)
33

34
const (
35
        renderArtifactFinalizer = "solar.opendefense.cloud/render-artifact-finalizer"
36
        ConditionTypeOCICleanup = "OCICleanup"
37
)
38

39
// RenderArtifactReconciler reconciles RenderArtifact objects.
40
// It sets status.ChartURL and acts as the GC controller: when the last RenderBinding
41
// referencing a RenderArtifact is removed, it attempts to delete the OCI tag
42
// and then deletes the RenderArtifact object itself.
43
//
44
// OCI tag deletion failures are surfaced as a status condition and a Warning event
45
// so users have visibility; the finalizer is kept until the deletion succeeds,
46
// making the artifact object "stuck" in a visible state.
47
type RenderArtifactReconciler struct {
48
        client.Client
49
        Scheme    *runtime.Scheme
50
        Recorder  events.EventRecorder
51
        APIReader client.Reader
52
        // DeleteTag overrides the OCI tag deletion function used during GC.
53
        // Defaults to ociregistry.DeleteTag; replaced in tests.
54
        DeleteTag func(ctx context.Context, rawRef string, auth authn.Authenticator, insecure bool) error
55
        // WatchNamespace restricts reconciliation to this namespace.
56
        // Should be empty in production (watches all namespaces).
57
        // Intended for use in integration tests only.
58
        WatchNamespace string
59
}
60

61
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=renderartifacts,verbs=get;list;watch;update;patch;delete
62
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=renderartifacts/status,verbs=get;update;patch
63
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=renderartifacts/finalizers,verbs=update
64
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=renderbindings,verbs=get;list;watch
65
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=registries,verbs=get
66
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=referencegrants,verbs=get;list
67
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get
68

69
func (r *RenderArtifactReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
184✔
70
        log := ctrl.LoggerFrom(ctx)
184✔
71

184✔
72
        log.V(1).Info("RenderArtifact is being reconciled", "req", req)
184✔
73

184✔
74
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
232✔
75
                return ctrl.Result{}, nil
48✔
76
        }
48✔
77

78
        artifact := &solarv1alpha1.RenderArtifact{}
136✔
79
        if err := r.Get(ctx, req.NamespacedName, artifact); err != nil {
162✔
80
                if apierrors.IsNotFound(err) {
52✔
81
                        return ctrl.Result{}, nil
26✔
82
                }
26✔
83

84
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get RenderArtifact")
×
85
        }
86

87
        // Handle deletion: attempt OCI tag cleanup, surface errors explicitly, then remove finalizer.
88
        if !artifact.DeletionTimestamp.IsZero() {
143✔
89
                if slices.Contains(artifact.Finalizers, renderArtifactFinalizer) {
66✔
90
                        bound, err := r.renderArtifactBound(ctx, artifact)
33✔
91
                        if err != nil {
33✔
NEW
92
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to re-check RenderBindings for terminating RenderArtifact")
×
UNCOV
93
                        }
×
94

95
                        if bound {
35✔
96
                                // A binding still references this artifact: someone still needs the OCI tag,
2✔
97
                                // so keep it. The finalizer is retained on purpose: as long as the binding
2✔
98
                                // exists the object must not be deleted, or the tag would be orphaned in the
2✔
99
                                // registry (e.g. during namespace teardown nothing would recreate it). Once
2✔
100
                                // the binding is gone (Target deletion removes its bindings), a follow-up
2✔
101
                                // reconcile takes the not-bound path below and cleans up the tag.
2✔
102
                                log.V(1).Info("RenderArtifact is terminating but still referenced by a RenderBinding; keeping OCI tag",
2✔
103
                                        "artifact", artifact.Name)
2✔
104
                                r.Recorder.Eventf(artifact, nil, corev1.EventTypeNormal, "OCICleanupSkipped", "Delete",
2✔
105
                                        "RenderArtifact is terminating but still referenced by a RenderBinding; keeping OCI tag")
2✔
106

2✔
107
                                return ctrl.Result{}, nil
2✔
108
                        } else {
33✔
109
                                if err := r.cleanupOCIArtifact(ctx, artifact); err != nil {
53✔
110
                                        // Failure is already logged + event fired inside cleanupOCIArtifact.
22✔
111
                                        // Keep the finalizer by returning the error so the object stays visible
22✔
112
                                        // with the OCICleanup=False condition set.
22✔
113
                                        return ctrl.Result{}, err
22✔
114
                                }
22✔
115
                        }
116

117
                        // Remove finalizer to allow K8s deletion.
118
                        latest := artifact.DeepCopy()
9✔
119
                        latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool {
18✔
120
                                return s == renderArtifactFinalizer
9✔
121
                        })
9✔
122
                        if err := r.Patch(ctx, latest, client.MergeFrom(artifact)); err != nil {
9✔
123
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to remove finalizer from RenderArtifact")
×
124
                        }
×
125
                }
126

127
                return ctrl.Result{}, nil
9✔
128
        }
129

130
        // Ensure finalizer is set.
131
        if !slices.Contains(artifact.Finalizers, renderArtifactFinalizer) {
102✔
132
                latest := artifact.DeepCopy()
25✔
133
                latest.Finalizers = append(latest.Finalizers, renderArtifactFinalizer)
25✔
134
                if err := r.Patch(ctx, latest, client.MergeFrom(artifact)); err != nil {
25✔
135
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to RenderArtifact")
×
136
                }
×
137

138
                return ctrl.Result{}, nil
25✔
139
        }
140

141
        // Populate status.ChartURL from spec coordinates if not yet set.
142
        chartURL := renderChartURL(artifact.Spec.BaseURL, artifact.Spec.Repository, artifact.Spec.Tag)
52✔
143
        if artifact.Status.ChartURL != chartURL {
77✔
144
                base := artifact.DeepCopy()
25✔
145
                artifact.Status.ChartURL = chartURL
25✔
146
                if err := r.Status().Patch(ctx, artifact, client.MergeFrom(base)); err != nil {
25✔
147
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to update RenderArtifact status")
×
148
                }
×
149
        }
150

151
        // List RenderBindings referencing this artifact.
152
        bindingList := &solarv1alpha1.RenderBindingList{}
52✔
153
        if err := r.List(ctx, bindingList,
52✔
154
                client.InNamespace(artifact.Namespace),
52✔
155
                client.MatchingFields{indexRenderBindingArtifactName: artifact.Name},
52✔
156
        ); err != nil {
52✔
157
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list RenderBindings for RenderArtifact")
×
158
        }
×
159

160
        if len(bindingList.Items) > 0 {
92✔
161
                // While at least one binding exists, keep the artifact's RegistryRef pinned to
40✔
162
                // a binding that still exists.
40✔
163
                if err := r.repinCredentials(ctx, artifact, bindingList.Items); err != nil {
40✔
164
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to re-pin RenderArtifact credentials")
×
165
                }
×
166
        } else {
12✔
167
                // If no bindings remain, trigger GC by deleting this object.
12✔
168
                // The finalizer above will intercept the deletion and handle OCI cleanup.
12✔
169
                // Confirm via direct API call — cache may lag on concurrent creates.
12✔
170
                confirmed := &solarv1alpha1.RenderBindingList{}
12✔
171
                if err := r.APIReader.List(ctx, confirmed, client.InNamespace(artifact.Namespace)); err != nil {
12✔
172
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to confirm RenderBinding absence via API")
×
173
                }
×
174
                for i := range confirmed.Items {
20✔
175
                        if confirmed.Items[i].Spec.RenderArtifactRef.Name == artifact.Name {
8✔
176
                                // A binding exists in the API server that the cache missed.
×
177
                                return ctrl.Result{}, nil
×
178
                        }
×
179
                }
180
                log.V(1).Info("No RenderBindings remain for RenderArtifact — triggering GC",
12✔
181
                        "artifact", artifact.Name)
12✔
182
                if err := r.Delete(ctx, artifact); client.IgnoreNotFound(err) != nil {
12✔
183
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete orphaned RenderArtifact")
×
184
                }
×
185
        }
186

187
        return ctrl.Result{}, nil
52✔
188
}
189

190
// renderArtifactBound reports whether any RenderBinding still references the artifact.
191
// Used during deletion so the OCI tag is not deleted while a Target still needs it.
192
// Confirms via APIReader because the cache may lag on concurrent binding creates.
193
func (r *RenderArtifactReconciler) renderArtifactBound(ctx context.Context, artifact *solarv1alpha1.RenderArtifact) (bool, error) {
33✔
194
        bindingList := &solarv1alpha1.RenderBindingList{}
33✔
195
        if err := r.List(ctx, bindingList,
33✔
196
                client.InNamespace(artifact.Namespace),
33✔
197
                client.MatchingFields{indexRenderBindingArtifactName: artifact.Name},
33✔
198
        ); err != nil {
33✔
NEW
199
                return false, err
×
NEW
200
        }
×
201
        if len(bindingList.Items) > 0 {
34✔
202
                return true, nil
1✔
203
        }
1✔
204

205
        confirmed := &solarv1alpha1.RenderBindingList{}
32✔
206
        if err := r.APIReader.List(ctx, confirmed, client.InNamespace(artifact.Namespace)); err != nil {
32✔
NEW
207
                return false, err
×
NEW
208
        }
×
209
        for i := range confirmed.Items {
57✔
210
                if confirmed.Items[i].Spec.RenderArtifactRef.Name == artifact.Name {
26✔
211
                        return true, nil
1✔
212
                }
1✔
213
        }
214

215
        return false, nil
31✔
216
}
217

218
// cleanupOCIArtifact attempts to delete the OCI tag from the registry.
219
// On failure it sets a status condition and fires a Warning event so the user
220
// can see why the RenderArtifact is stuck, then returns the error to keep the
221
// finalizer in place.
222
func (r *RenderArtifactReconciler) cleanupOCIArtifact(ctx context.Context, artifact *solarv1alpha1.RenderArtifact) error {
31✔
223
        log := ctrl.LoggerFrom(ctx)
31✔
224

31✔
225
        registryHost := normalizeRegistryHost(artifact.Spec.BaseURL)
31✔
226
        rawRef := registryHost + "/" + strings.TrimPrefix(artifact.Spec.Repository, "/") + ":" + artifact.Spec.Tag
31✔
227
        log.V(1).Info("Attempting OCI tag cleanup", "ref", rawRef)
31✔
228

31✔
229
        deleteFn := r.DeleteTag
31✔
230
        if deleteFn == nil {
31✔
231
                deleteFn = ociregistry.DeleteTag
×
232
        }
×
233

234
        auth, plainHTTP, err := r.resolveAuth(ctx, artifact, registryHost)
31✔
235
        if err != nil {
46✔
236
                log.Error(err, "Failed to resolve OCI auth; RenderArtifact will remain until secret is accessible",
15✔
237
                        "artifact", artifact.Name)
15✔
238
                r.Recorder.Eventf(artifact, nil, corev1.EventTypeWarning,
15✔
239
                        "OCICleanupFailed", "Delete",
15✔
240
                        "Failed to resolve OCI auth for %s: %s", rawRef, err.Error())
15✔
241

15✔
242
                latest := artifact.DeepCopy()
15✔
243
                apimeta.SetStatusCondition(&latest.Status.Conditions, metav1.Condition{
15✔
244
                        Type:               ConditionTypeOCICleanup,
15✔
245
                        Status:             metav1.ConditionFalse,
15✔
246
                        ObservedGeneration: artifact.Generation,
15✔
247
                        Reason:             "AuthFailed",
15✔
248
                        Message:            err.Error(),
15✔
249
                })
15✔
250
                if sErr := r.Status().Patch(ctx, latest, client.MergeFrom(artifact)); sErr != nil {
15✔
251
                        log.Error(sErr, "failed to update status condition after OCI auth failure")
×
252
                }
×
253

254
                return err
15✔
255
        }
256

257
        deleteCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
16✔
258
        defer cancel()
16✔
259
        if err := deleteFn(deleteCtx, rawRef, auth, plainHTTP); err != nil {
24✔
260
                // If the tag is already gone, proceed normally.
8✔
261
                var transportErr *transport.Error
8✔
262
                if errors.As(err, &transportErr) && transportErr.StatusCode == http.StatusNotFound {
9✔
263
                        log.V(1).Info("OCI tag already absent — skipping delete", "ref", rawRef)
1✔
264
                        return nil
1✔
265
                }
1✔
266

267
                log.Error(err, "Failed to delete OCI tag; RenderArtifact will remain until deletion succeeds",
7✔
268
                        "ref", rawRef, "artifact", artifact.Name)
7✔
269
                r.Recorder.Eventf(artifact, nil, corev1.EventTypeWarning,
7✔
270
                        "OCICleanupFailed", "Delete",
7✔
271
                        "Failed to delete OCI tag %s: %s", rawRef, err.Error())
7✔
272

7✔
273
                latest := artifact.DeepCopy()
7✔
274
                apimeta.SetStatusCondition(&latest.Status.Conditions, metav1.Condition{
7✔
275
                        Type:               ConditionTypeOCICleanup,
7✔
276
                        Status:             metav1.ConditionFalse,
7✔
277
                        ObservedGeneration: artifact.Generation,
7✔
278
                        Reason:             "DeleteFailed",
7✔
279
                        Message:            err.Error(),
7✔
280
                })
7✔
281
                // Status patch, if it fails, the event + log are visible in kubectl
7✔
282
                if sErr := r.Status().Patch(ctx, latest, client.MergeFrom(artifact)); sErr != nil {
8✔
283
                        log.Error(sErr, "failed to update status condition after OCI cleanup failure")
1✔
284
                }
1✔
285

286
                return err
7✔
287
        }
288

289
        log.V(1).Info("OCI tag deleted successfully", "ref", rawRef)
8✔
290
        r.Recorder.Eventf(artifact, nil, corev1.EventTypeNormal,
8✔
291
                "OCICleanupSucceeded", "Delete",
8✔
292
                "Successfully deleted OCI tag %s", rawRef)
8✔
293

8✔
294
        return nil
8✔
295
}
296

297
// repinCredentials keeps artifact.Spec.RegistryRef pinned to the Registry snapshotted on
298
// a still-existing RenderBinding. Bindings that carry no RegistryRef are not candidates;
299
// among the rest the lowest name wins, so repeated reconciles converge instead of flapping
300
// between equally-valid choices.
301
// Because this runs on every RenderBinding create/update/delete event (see
302
// mapRenderBindingToArtifact), the artifact's pinned RegistryRef is always synced to a
303
// binding that exists, including immediately after the second-to-last binding is
304
// removed, which is exactly the moment that matters: it leaves the artifact holding a
305
// Registry reference that was valid for the binding that survives until the final
306
// removal, which is what the finalizer step needs to delete the OCI tag.
307
func (r *RenderArtifactReconciler) repinCredentials(ctx context.Context, artifact *solarv1alpha1.RenderArtifact, bindings []solarv1alpha1.RenderBinding) error {
40✔
308
        slices.SortFunc(bindings, func(a, b solarv1alpha1.RenderBinding) int { return strings.Compare(a.Name, b.Name) })
42✔
309

310
        // RegistryRef is optional, so bindings written before it existed carry nil. Skip those
311
        // instead of pinning nil over a working reference
312
        // If no binding carries a reference, keep what the artifact
313
        // already has: a stale-but-valid ref deletes the tag, nil does not.
314
        idx := slices.IndexFunc(bindings, func(b solarv1alpha1.RenderBinding) bool {
82✔
315
                return b.Spec.RegistryRef != nil
42✔
316
        })
42✔
317
        if idx < 0 {
49✔
318
                return nil
9✔
319
        }
9✔
320
        chosen := bindings[idx]
31✔
321

31✔
322
        if registryRefEqual(artifact.Spec.RegistryRef, chosen.Spec.RegistryRef) {
59✔
323
                return nil
28✔
324
        }
28✔
325

326
        latest := artifact.DeepCopy()
3✔
327
        latest.Spec.RegistryRef = chosen.Spec.RegistryRef
3✔
328

3✔
329
        return r.Patch(ctx, latest, client.MergeFrom(artifact))
3✔
330
}
331

332
func registryRefEqual(a, b *solarv1alpha1.ObjectReference) bool {
78✔
333
        if a == nil || b == nil {
78✔
334
                return a == b
×
335
        }
×
336

337
        return *a == *b
78✔
338
}
339

340
func (r *RenderArtifactReconciler) resolveAuth(ctx context.Context, artifact *solarv1alpha1.RenderArtifact, registryHost string) (authn.Authenticator, bool, error) {
36✔
341
        log := ctrl.LoggerFrom(ctx)
36✔
342

36✔
343
        if artifact.Spec.RegistryRef == nil {
50✔
344
                return authn.Anonymous, false, nil
14✔
345
        }
14✔
346

347
        registryNamespace := artifact.Namespace
22✔
348
        if artifact.Spec.RegistryRef.Namespace != "" {
25✔
349
                registryNamespace = artifact.Spec.RegistryRef.Namespace
3✔
350
        }
3✔
351

352
        // RegistryRef is meant to be controller-owned, but nothing stops a principal with
353
        // create/update on RenderArtifact from authoring one. Riding the Target's grant would
354
        // then hand those credentials to anyone who can write a RenderArtifact in a namespace
355
        // some Target happens to be granted from, so the grant must name RenderArtifact itself.
356
        if registryNamespace != artifact.Namespace {
25✔
357
                granted, err := registryGranted(ctx, r.APIReader, registryNamespace, "RenderArtifact", artifact.Namespace)
3✔
358
                if err != nil {
3✔
359
                        return nil, false, fmt.Errorf("failed to check ReferenceGrant for Registry %s/%s: %w",
×
360
                                registryNamespace, artifact.Spec.RegistryRef.Name, err)
×
361
                }
×
362
                if !granted {
5✔
363
                        return nil, false, fmt.Errorf(
2✔
364
                                "no ReferenceGrant in namespace %s with from[].kind=RenderArtifact, from[].namespace=%s and to[].kind=Registry "+
2✔
365
                                        "allows RenderArtifact %s/%s to access Registry %s/%s",
2✔
366
                                registryNamespace, artifact.Namespace,
2✔
367
                                artifact.Namespace, artifact.Name, registryNamespace, artifact.Spec.RegistryRef.Name)
2✔
368
                }
2✔
369
        }
370

371
        registry := &solarv1alpha1.Registry{}
20✔
372
        if err := r.APIReader.Get(ctx, client.ObjectKey{
20✔
373
                Name:      artifact.Spec.RegistryRef.Name,
20✔
374
                Namespace: registryNamespace,
20✔
375
        }, registry); err != nil {
20✔
376
                return nil, false, fmt.Errorf("failed to get Registry %s/%s: %w", registryNamespace, artifact.Spec.RegistryRef.Name, err)
×
377
        }
×
378

379
        // The grant authorizes this namespace to use the Registry, not to use its credentials
380
        // against an arbitrary host. spec.baseURL is what the delete is aimed at, and a Registry
381
        // secret may hold auths for several hosts, so refuse unless the artifact points at the
382
        // Registry's own hostname. Artifacts the Target controller produced always do
383
        if artifactHost, registryHostname := registryHost, normalizeRegistryHost(registry.Spec.Hostname); artifactHost != registryHostname {
21✔
384
                return nil, false, fmt.Errorf(
1✔
385
                        "RenderArtifact %s/%s targets host %q but Registry %s/%s serves %q; refusing to use its credentials",
1✔
386
                        artifact.Namespace, artifact.Name, artifactHost, registryNamespace, registry.Name, registryHostname)
1✔
387
        }
1✔
388

389
        if registry.Spec.SolarSecretRef == nil {
20✔
390
                return authn.Anonymous, registry.Spec.PlainHTTP, nil
1✔
391
        }
1✔
392

393
        secret := &corev1.Secret{}
18✔
394
        if err := r.Get(ctx, client.ObjectKey{
18✔
395
                Name:      registry.Spec.SolarSecretRef.Name,
18✔
396
                Namespace: registry.Namespace,
18✔
397
        }, secret); err != nil {
33✔
398
                log.Error(err, "Failed to get push secret for OCI auth",
15✔
399
                        "secret", registry.Spec.SolarSecretRef.Name)
15✔
400

15✔
401
                return nil, false, fmt.Errorf("failed to get push secret %s/%s: %w", registry.Namespace, registry.Spec.SolarSecretRef.Name, err)
15✔
402
        }
15✔
403

404
        auth, err := ociAuthFromSecret(secret, registryHost)
3✔
405
        if err != nil {
3✔
406
                // A malformed dockerconfigjson is a configuration error; log it so the operator
×
407
                // is aware, but fall back to anonymous rather than blocking OCI cleanup.
×
408
                log.Error(err, "Malformed push secret; falling back to anonymous OCI auth",
×
409
                        "secret", fmt.Sprintf("%s/%s", registry.Namespace, registry.Spec.SolarSecretRef.Name))
×
410
        }
×
411

412
        return auth, registry.Spec.PlainHTTP, nil
3✔
413
}
414

415
// normalizeRegistryHost strips the oci:// scheme and any trailing slash so a
416
// Registry hostname and an artifact baseURL can be compared as written by either side.
417
func normalizeRegistryHost(s string) string {
51✔
418
        return strings.TrimPrefix(strings.TrimSuffix(s, "/"), "oci://")
51✔
419
}
51✔
420

421
// ociAuthFromSecret extracts OCI credentials from a Kubernetes Secret.
422
// callers should log the error and decide whether to fall back to anonymous or abort.
423
func ociAuthFromSecret(secret *corev1.Secret, registryHost string) (authn.Authenticator, error) {
3✔
424
        if secret.Type == corev1.SecretTypeBasicAuth {
6✔
425
                user := string(secret.Data["username"])
3✔
426
                pass := string(secret.Data["password"])
3✔
427
                if user != "" || pass != "" {
6✔
428
                        return authn.FromConfig(authn.AuthConfig{Username: user, Password: pass}), nil
3✔
429
                }
3✔
430

431
                return authn.Anonymous, nil
×
432
        }
433

434
        data := secret.Data[corev1.DockerConfigJsonKey]
×
435
        if len(data) == 0 {
×
436
                return authn.Anonymous, nil
×
437
        }
×
438

439
        var cfg struct {
×
440
                Auths map[string]authn.AuthConfig `json:"auths"`
×
441
        }
×
442
        if err := json.Unmarshal(data, &cfg); err != nil {
×
443
                return authn.Anonymous, fmt.Errorf("failed to parse dockerconfigjson in secret %s/%s: %w", secret.Namespace, secret.Name, err)
×
444
        }
×
445

446
        if ac, ok := cfg.Auths[registryHost]; ok {
×
447
                return authn.FromConfig(ac), nil
×
448
        }
×
449

450
        if ac, ok := cfg.Auths["https://"+registryHost]; ok {
×
451
                return authn.FromConfig(ac), nil
×
452
        }
×
453

454
        return authn.Anonymous, nil
×
455
}
456

457
// mapRenderBindingToArtifact maps a RenderBinding event to a reconcile request
458
// for the RenderArtifact it references, so the GC controller is triggered on
459
// every RenderBinding deletion.
460
func mapRenderBindingToArtifact(_ context.Context, obj client.Object) []reconcile.Request {
48✔
461
        rb, ok := obj.(*solarv1alpha1.RenderBinding)
48✔
462
        if !ok {
48✔
463
                return nil
×
464
        }
×
465

466
        if rb.Spec.RenderArtifactRef.Name == "" {
48✔
467
                return nil
×
468
        }
×
469

470
        return []reconcile.Request{
48✔
471
                {
48✔
472
                        NamespacedName: types.NamespacedName{
48✔
473
                                Name:      rb.Spec.RenderArtifactRef.Name,
48✔
474
                                Namespace: rb.Namespace,
48✔
475
                        },
48✔
476
                },
48✔
477
        }
48✔
478
}
479

480
// SetupWithManager sets up the controller with the Manager.
481
func (r *RenderArtifactReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
482
        return ctrl.NewControllerManagedBy(mgr).
1✔
483
                For(&solarv1alpha1.RenderArtifact{}).
1✔
484
                Watches(
1✔
485
                        &solarv1alpha1.RenderBinding{},
1✔
486
                        handler.EnqueueRequestsFromMapFunc(mapRenderBindingToArtifact),
1✔
487
                ).
1✔
488
                Complete(r)
1✔
489
}
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