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

opendefensecloud / artifact-conduit / 19627192803

24 Nov 2025 07:56AM UTC coverage: 61.516% (-0.3%) from 61.808%
19627192803

push

github

jastBytes
ghactions: bump actions/checkout from 5 to 6

Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

422 of 686 relevant lines covered (61.52%)

596.12 hits per line

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

82.83
/pkg/controller/order_controller.go
1
// Copyright 2025 BWI GmbH and Artifact Conduit contributors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package controller
5

6
import (
7
        "context"
8
        "crypto/sha256"
9
        "encoding/hex"
10
        "encoding/json"
11
        "fmt"
12
        "slices"
13
        "strconv"
14
        "strings"
15

16
        arcv1alpha1 "go.opendefense.cloud/arc/api/arc/v1alpha1"
17
        corev1 "k8s.io/api/core/v1"
18
        apierrors "k8s.io/apimachinery/pkg/api/errors"
19
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20
        "k8s.io/apimachinery/pkg/fields"
21
        "k8s.io/apimachinery/pkg/runtime"
22
        "k8s.io/apimachinery/pkg/types"
23
        ctrl "sigs.k8s.io/controller-runtime"
24
        "sigs.k8s.io/controller-runtime/pkg/builder"
25
        "sigs.k8s.io/controller-runtime/pkg/client"
26
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
27
        "sigs.k8s.io/controller-runtime/pkg/handler"
28
        "sigs.k8s.io/controller-runtime/pkg/predicate"
29
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
30
)
31

32
const (
33
        orderFinalizer = "arc.bwi.de/order-finalizer"
34
)
35

36
// OrderReconciler reconciles a Order object
37
type OrderReconciler struct {
38
        client.Client
39
        Scheme *runtime.Scheme
40
}
41

42
type desiredAW struct {
43
        index       int
44
        objectMeta  metav1.ObjectMeta
45
        artifact    *arcv1alpha1.OrderArtifact
46
        srcEndpoint *arcv1alpha1.Endpoint
47
        dstEndpoint *arcv1alpha1.Endpoint
48
        srcSecret   *corev1.Secret
49
        dstSecret   *corev1.Secret
50
}
51

52
//+kubebuilder:rbac:groups=arc.bwi.de,resources=endpoints,verbs=get;list;watch
53
//+kubebuilder:rbac:groups=arc.bwi.de,resources=artifactworkflows,verbs=get;list;watch;create;update;patch;delete
54
//+kubebuilder:rbac:groups=arc.bwi.de,resources=orders,verbs=get;list;watch;create;update;patch;delete
55
//+kubebuilder:rbac:groups=arc.bwi.de,resources=orders/status,verbs=get;update;patch
56
//+kubebuilder:rbac:groups=arc.bwi.de,resources=orders/finalizers,verbs=update
57
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
58

59
// Reconcile moves the current state of the cluster closer to the desired state
60
func (r *OrderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
1,412✔
61
        log := ctrl.LoggerFrom(ctx)
1,412✔
62

1,412✔
63
        // Fetch the Order instance
1,412✔
64
        order := &arcv1alpha1.Order{}
1,412✔
65
        if err := r.Get(ctx, req.NamespacedName, order); err != nil {
1,416✔
66
                if apierrors.IsNotFound(err) {
8✔
67
                        // Object not found, return. Created objects are automatically garbage collected.
4✔
68
                        return ctrl.Result{}, nil
4✔
69
                }
4✔
70
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get object")
×
71
        }
72

73
        // Handle deletion: cleanup fragments, then remove finalizer
74
        if !order.DeletionTimestamp.IsZero() {
1,410✔
75
                log.V(1).Info("Order is being deleted")
2✔
76
                if len(order.Status.ArtifactWorkflows) > 0 {
3✔
77
                        for sha := range order.Status.ArtifactWorkflows {
3✔
78
                                // Remove Secret and ArtifactWorkflow
2✔
79
                                aw := &arcv1alpha1.ArtifactWorkflow{
2✔
80
                                        ObjectMeta: awObjectMeta(order, sha),
2✔
81
                                }
2✔
82
                                _ = r.Delete(ctx, aw) // Ignore errors
2✔
83
                                delete(order.Status.ArtifactWorkflows, sha)
2✔
84
                        }
2✔
85
                        if err := r.Status().Update(ctx, order); err != nil {
1✔
86
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update order status")
×
87
                        }
×
88
                        log.V(1).Info("Order artifact workflows cleaned up")
1✔
89
                        // Requeue until all fragments are gone
1✔
90
                        return ctrl.Result{Requeue: true}, nil
1✔
91
                }
92
                // All fragments are gone, remove finalizer
93
                if slices.Contains(order.Finalizers, orderFinalizer) {
2✔
94
                        log.V(1).Info("No artifact workflows, removing finalizer from Order")
1✔
95
                        order.Finalizers = slices.DeleteFunc(order.Finalizers, func(f string) bool {
2✔
96
                                return f == orderFinalizer
1✔
97
                        })
1✔
98
                        if err := r.Update(ctx, order); err != nil {
1✔
99
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to remove finalizer")
×
100
                        }
×
101
                }
102
                return ctrl.Result{}, nil
1✔
103
        }
104

105
        // Add finalizer if not present and not deleting
106
        if order.DeletionTimestamp.IsZero() {
2,812✔
107
                if !slices.Contains(order.Finalizers, orderFinalizer) {
1,414✔
108
                        log.V(1).Info("Adding finalizer to Order")
8✔
109
                        order.Finalizers = append(order.Finalizers, orderFinalizer)
8✔
110
                        if err := r.Update(ctx, order); err != nil {
8✔
111
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer")
×
112
                        }
×
113
                        // Return without requeue; the Update event will trigger reconciliation again
114
                        return ctrl.Result{}, nil
8✔
115
                }
116
        }
117

118
        // Before we compare to our status, let's fetch all necessary information
119
        // to compute desired state:
120
        desiredAWs := map[string]desiredAW{}
1,398✔
121
        for i, artifact := range order.Spec.Artifacts {
3,790✔
122
                // TODO: When a endpoint or secret fetch fails, we stop the reconciliation of the whole order.
2,392✔
123
                //       Should we instead not fail but skip invalid artifacts?
2,392✔
124
                log := log.WithValues("artifactIndex", i)
2,392✔
125

2,392✔
126
                // We need the referenced src- and dst-endpoints for the artifact
2,392✔
127
                srcRefName := artifact.SrcRef.Name
2,392✔
128
                if srcRefName == "" {
2,958✔
129
                        srcRefName = order.Spec.Defaults.SrcRef.Name
566✔
130
                }
566✔
131
                dstRefName := artifact.DstRef.Name
2,392✔
132
                if dstRefName == "" {
3,140✔
133
                        dstRefName = order.Spec.Defaults.DstRef.Name
748✔
134
                }
748✔
135
                srcEndpoint := &arcv1alpha1.Endpoint{}
2,392✔
136
                if err := r.Get(ctx, namespacedName(order.Namespace, srcRefName), srcEndpoint); err != nil {
2,392✔
137
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch endpoint for source")
×
138
                }
×
139
                dstEndpoint := &arcv1alpha1.Endpoint{}
2,392✔
140
                if err := r.Get(ctx, namespacedName(order.Namespace, dstRefName), dstEndpoint); err != nil {
2,392✔
141
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch endpoint for destination")
×
142
                }
×
143

144
                // Next, we need the secret contents
145
                srcSecret := &corev1.Secret{}
2,392✔
146
                if srcEndpoint.Spec.SecretRef.Name != "" {
4,781✔
147
                        if err := r.Get(ctx, namespacedName(order.Namespace, srcEndpoint.Spec.SecretRef.Name), srcSecret); err != nil {
2,389✔
148
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch secret for source")
×
149
                        }
×
150
                }
151

152
                dstSecret := &corev1.Secret{}
2,392✔
153
                if dstEndpoint.Spec.SecretRef.Name != "" {
4,781✔
154
                        if err := r.Get(ctx, namespacedName(order.Namespace, dstEndpoint.Spec.SecretRef.Name), dstSecret); err != nil {
2,389✔
155
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch secret for destination")
×
156
                        }
×
157
                }
158

159
                // Create a hash based on all related data for idempotency and compute the workflow name
160
                h := sha256.New()
2,392✔
161
                data := []any{
2,392✔
162
                        order.Namespace,
2,392✔
163
                        artifact.Type, artifact.Spec.Raw,
2,392✔
164
                        srcEndpoint.Name, srcEndpoint.Generation,
2,392✔
165
                        dstEndpoint.Name, dstEndpoint.Generation,
2,392✔
166
                        srcSecret.Name, srcSecret.Generation,
2,392✔
167
                        dstSecret.Name, dstSecret.Generation,
2,392✔
168
                }
2,392✔
169
                jsonData, err := json.Marshal(data)
2,392✔
170
                if err != nil {
2,392✔
171
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to marshal fragment data")
×
172
                }
×
173
                h.Write(jsonData)
2,392✔
174
                sha := hex.EncodeToString(h.Sum(nil))[:16]
2,392✔
175

2,392✔
176
                // We gave all the information to further process this artifact workflow.
2,392✔
177
                // Let's store it to compare it to the current status!
2,392✔
178
                desiredAWs[sha] = desiredAW{
2,392✔
179
                        index:       i,
2,392✔
180
                        objectMeta:  awObjectMeta(order, sha),
2,392✔
181
                        artifact:    &artifact,
2,392✔
182
                        srcEndpoint: srcEndpoint,
2,392✔
183
                        dstEndpoint: dstEndpoint,
2,392✔
184
                        srcSecret:   srcSecret,
2,392✔
185
                        dstSecret:   dstSecret,
2,392✔
186
                }
2,392✔
187
        }
188

189
        // List missing fragments
190
        createAWs := []string{}
1,398✔
191
        for sha := range desiredAWs {
3,790✔
192
                _, exists := order.Status.ArtifactWorkflows[sha]
2,392✔
193
                if exists {
4,769✔
194
                        continue
2,377✔
195
                }
196
                createAWs = append(createAWs, sha)
15✔
197
        }
198

199
        // Make sure status is initialized
200
        if order.Status.ArtifactWorkflows == nil {
1,406✔
201
                order.Status.ArtifactWorkflows = map[string]arcv1alpha1.OrderArtifactWorkflowStatus{}
8✔
202
        }
8✔
203

204
        // Find obsolete fragments
205
        deleteAWs := []string{}
1,398✔
206
        for sha := range order.Status.ArtifactWorkflows {
3,776✔
207
                _, exists := desiredAWs[sha]
2,378✔
208
                if exists {
4,755✔
209
                        continue
2,377✔
210
                }
211
                deleteAWs = append(deleteAWs, sha)
1✔
212
        }
213

214
        // Create missing fragments
215
        for _, sha := range createAWs {
1,413✔
216
                daw := desiredAWs[sha]
15✔
217
                aw, err := r.hydrateArtifactWorkflow(&daw)
15✔
218
                if err != nil {
15✔
219
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to hydrate artifact workflow")
×
220
                }
×
221

222
                // Set owner references
223
                if err := controllerutil.SetControllerReference(order, aw, r.Scheme); err != nil {
15✔
224
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to set controller reference")
×
225
                }
×
226

227
                // Create artifact workflow
228
                if err := r.Create(ctx, aw); err != nil {
15✔
229
                        if apierrors.IsAlreadyExists(err) {
×
230
                                // Already created by a previous reconcile — that's fine
×
231
                                continue
×
232
                        }
233
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to create artifact workflow")
×
234
                }
235

236
                // Update status
237
                order.Status.ArtifactWorkflows[sha] = arcv1alpha1.OrderArtifactWorkflowStatus{
15✔
238
                        ArtifactIndex: daw.index,
15✔
239
                        Phase:         arcv1alpha1.WorkflowUnknown,
15✔
240
                }
15✔
241
        }
242

243
        // Delete obsolete fragments
244
        for _, sha := range deleteAWs {
1,399✔
245
                // Does not exist anymore, let's clean up!
1✔
246
                if err := r.Delete(ctx, &arcv1alpha1.ArtifactWorkflow{
1✔
247
                        ObjectMeta: awObjectMeta(order, sha),
1✔
248
                }); client.IgnoreNotFound(err) != nil {
1✔
249
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete artifact workflow")
×
250
                }
×
251

252
                // Update status
253
                delete(order.Status.ArtifactWorkflows, sha)
1✔
254
        }
255

256
        anyPhaseChanged := false
1,398✔
257
        for sha, daw := range desiredAWs {
3,790✔
258
                if slices.Contains(createAWs, sha) {
2,407✔
259
                        // If it was just created we skip the update
15✔
260
                        continue
15✔
261
                }
262
                aw := arcv1alpha1.ArtifactWorkflow{}
2,377✔
263
                if err := r.Get(ctx, namespacedName(daw.objectMeta.Namespace, daw.objectMeta.Name), &aw); err != nil {
2,377✔
264
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get artifact workflow")
×
265
                }
×
266
                if order.Status.ArtifactWorkflows[sha].Phase != aw.Status.Phase {
3,289✔
267
                        awStatus := order.Status.ArtifactWorkflows[sha]
912✔
268
                        awStatus.Phase = aw.Status.Phase
912✔
269
                        order.Status.ArtifactWorkflows[sha] = awStatus
912✔
270
                        anyPhaseChanged = true
912✔
271
                }
912✔
272
        }
273

274
        // Update status
275
        if len(createAWs) > 0 || len(deleteAWs) > 0 || anyPhaseChanged {
2,303✔
276
                log.V(1).Info("Updating order status")
905✔
277
                // Make sure ArtifactIndex is up to date
905✔
278
                for sha, daw := range desiredAWs {
2,455✔
279
                        aws := order.Status.ArtifactWorkflows[sha]
1,550✔
280
                        aws.ArtifactIndex = daw.index
1,550✔
281
                        order.Status.ArtifactWorkflows[sha] = aws
1,550✔
282
                }
1,550✔
283
                if err := r.Status().Update(ctx, order); err != nil {
920✔
284
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to update status")
15✔
285
                }
15✔
286
        }
287

288
        return ctrl.Result{}, nil
1,383✔
289
}
290

291
func (r *OrderReconciler) hydrateArtifactWorkflow(daw *desiredAW) (*arcv1alpha1.ArtifactWorkflow, error) {
15✔
292
        params, err := dawToParameters(daw)
15✔
293
        if err != nil {
15✔
294
                return nil, err
×
295
        }
×
296

297
        // Next we create the ArtifactWorkflow instance
298
        aw := &arcv1alpha1.ArtifactWorkflow{
15✔
299
                ObjectMeta: daw.objectMeta,
15✔
300
                Spec: arcv1alpha1.ArtifactWorkflowSpec{
15✔
301
                        Type:         daw.artifact.Type,
15✔
302
                        Parameters:   params,
15✔
303
                        SrcSecretRef: daw.srcEndpoint.Spec.SecretRef,
15✔
304
                        DstSecretRef: daw.dstEndpoint.Spec.SecretRef,
15✔
305
                },
15✔
306
        }
15✔
307

15✔
308
        return aw, nil
15✔
309
}
310

311
// generateReconcileRequestsForEndpoint generates reconcile requests for all Endpoints referenced by an Order
312
func (r *OrderReconciler) generateReconcileRequestsForEndpoint(ctx context.Context, endpoint client.Object) []reconcile.Request {
25✔
313
        resourcesReferencingEndpoint := &arcv1alpha1.OrderList{}
25✔
314
        listOps := &client.ListOptions{
25✔
315
                FieldSelector: fields.SelectorFromSet(fields.Set{".spec.srcRef.name": endpoint.GetName(), ".spec.dstRef.name": endpoint.GetName()}),
25✔
316
                Namespace:     endpoint.GetNamespace(),
25✔
317
        }
25✔
318
        err := r.List(ctx, resourcesReferencingEndpoint, listOps)
25✔
319
        if err != nil {
50✔
320
                return []reconcile.Request{}
25✔
321
        }
25✔
322

323
        requests := make([]reconcile.Request, len(resourcesReferencingEndpoint.Items))
×
324
        for i, item := range resourcesReferencingEndpoint.Items {
×
325
                log := ctrl.LoggerFrom(ctx)
×
326
                log.V(1).Info("Generating reconcile request for resource because referenced endpoint has changed...")
×
327
                requests[i] = reconcile.Request{
×
328
                        NamespacedName: types.NamespacedName{
×
329
                                Name:      item.GetName(),
×
330
                                Namespace: item.GetNamespace(),
×
331
                        },
×
332
                }
×
333
        }
×
334
        return requests
×
335
}
336

337
// SetupWithManager sets up the controller with the Manager.
338
func (r *OrderReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
339
        return ctrl.NewControllerManagedBy(mgr).
1✔
340
                For(&arcv1alpha1.Order{}).
1✔
341
                Watches(
1✔
342
                        &arcv1alpha1.Endpoint{},
1✔
343
                        handler.EnqueueRequestsFromMapFunc(r.generateReconcileRequestsForEndpoint),
1✔
344
                        builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
1✔
345
                ).
1✔
346
                Owns(&arcv1alpha1.ArtifactWorkflow{}).
1✔
347
                Complete(r)
1✔
348
}
1✔
349

350
func namespacedName(namespace, name string) types.NamespacedName {
14,471✔
351
        return types.NamespacedName{
14,471✔
352
                Namespace: namespace,
14,471✔
353
                Name:      name,
14,471✔
354
        }
14,471✔
355
}
14,471✔
356

357
func awName(order *arcv1alpha1.Order, sha string) string {
2,395✔
358
        return fmt.Sprintf("%s-%s", order.Name, sha)
2,395✔
359
}
2,395✔
360

361
func awObjectMeta(order *arcv1alpha1.Order, sha string) metav1.ObjectMeta {
2,395✔
362
        return metav1.ObjectMeta{
2,395✔
363
                Namespace: order.Namespace,
2,395✔
364
                Name:      awName(order, sha),
2,395✔
365
        }
2,395✔
366
}
2,395✔
367

368
// TODO: add unit tests
369
func dawToParameters(daw *desiredAW) ([]arcv1alpha1.ArtifactWorkflowParameter, error) {
15✔
370
        params := []arcv1alpha1.ArtifactWorkflowParameter{
15✔
371
                {
15✔
372
                        Name:  paramName("src", "type"),
15✔
373
                        Value: daw.srcEndpoint.Spec.Type,
15✔
374
                },
15✔
375
                {
15✔
376
                        Name:  paramName("src", "remoteURL"),
15✔
377
                        Value: daw.srcEndpoint.Spec.RemoteURL,
15✔
378
                },
15✔
379
                {
15✔
380
                        Name:  paramName("dst", "type"),
15✔
381
                        Value: daw.dstEndpoint.Spec.Type,
15✔
382
                },
15✔
383
                {
15✔
384
                        Name:  paramName("dst", "remoteURL"),
15✔
385
                        Value: daw.dstEndpoint.Spec.RemoteURL,
15✔
386
                },
15✔
387
                {
15✔
388
                        Name:  "srcSecret",
15✔
389
                        Value: fmt.Sprintf("%v", daw.srcEndpoint.Spec.SecretRef.Name != ""),
15✔
390
                },
15✔
391
                {
15✔
392
                        Name:  "dstSecret",
15✔
393
                        Value: fmt.Sprintf("%v", daw.dstEndpoint.Spec.SecretRef.Name != ""),
15✔
394
                },
15✔
395
        }
15✔
396

15✔
397
        spec := map[string]any{}
15✔
398
        raw := daw.artifact.Spec.Raw
15✔
399
        if len(raw) == 0 {
21✔
400
                raw = []byte("{}")
6✔
401
        }
6✔
402
        if err := json.Unmarshal(raw, &spec); err != nil {
15✔
403
                return nil, err
×
404
        }
×
405
        flattened := map[string]any{}
15✔
406
        flattenMap("spec", spec, flattened)
15✔
407
        for name, value := range flattened {
24✔
408
                params = append(params, arcv1alpha1.ArtifactWorkflowParameter{
9✔
409
                        Name:  name,
9✔
410
                        Value: fmt.Sprintf("%v", value),
9✔
411
                })
9✔
412
        }
9✔
413

414
        return params, nil
15✔
415
}
416

417
// TODO: add unit tests
418
func paramName(prefix, suffix string) string {
60✔
419
        return prefix + strings.ToUpper(suffix[:1]) + suffix[1:]
60✔
420
}
60✔
421

422
// TODO: add unit tests
423
func flattenMap(prefix string, src map[string]any, dst map[string]any) {
15✔
424
        for k, v := range src {
24✔
425
                kt := strings.ToUpper(k[:1]) + k[1:]
9✔
426
                switch child := v.(type) {
9✔
427
                case map[string]any:
×
428
                        flattenMap(prefix+k, child, dst)
×
429
                case []any:
×
430
                        for i, av := range child {
×
431
                                dst[prefix+kt+strconv.Itoa(i)] = av
×
432
                        }
×
433
                default:
9✔
434
                        dst[prefix+kt] = v
9✔
435
                }
436
        }
437
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc