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

opendefensecloud / artifact-conduit / 19502856015

19 Nov 2025 01:24PM UTC coverage: 55.823% (+5.9%) from 49.926%
19502856015

push

github

jastBytes
Update pkg/controller/order_controller.go

1 of 1 new or added line in 1 file covered. (100.0%)

44 existing lines in 1 file now uncovered.

302 of 541 relevant lines covered (55.82%)

12.23 hits per line

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

82.97
/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
        workflowConfigSecretKey = "config.json"
35
)
36

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

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

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

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

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

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

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

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

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

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

153
                dstSecret := &corev1.Secret{}
43✔
154
                if srcEndpoint.Spec.SecretRef.Name != "" {
84✔
155
                        if err := r.Get(ctx, namespacedName(order.Namespace, dstEndpoint.Spec.SecretRef.Name), dstSecret); err != nil {
41✔
UNCOV
156
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch secret for destination")
×
UNCOV
157
                        }
×
158
                }
159

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

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

190
        // List missing fragments
191
        createAWs := []string{}
25✔
192
        for sha := range desiredAWs {
68✔
193
                _, exists := order.Status.ArtifactWorkflows[sha]
43✔
194
                if exists {
71✔
195
                        continue
28✔
196
                }
197
                createAWs = append(createAWs, sha)
15✔
198
        }
199

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

205
        // Find obsolete fragments
206
        deleteAWs := []string{}
25✔
207
        for sha := range order.Status.ArtifactWorkflows {
55✔
208
                _, exists := desiredAWs[sha]
30✔
209
                if exists {
58✔
210
                        continue
28✔
211
                }
212
                deleteAWs = append(deleteAWs, sha)
2✔
213
        }
214

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

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

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

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

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

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

257
        // Update status
258
        if len(createAWs) > 0 || len(deleteAWs) > 0 {
34✔
259
                log.V(1).Info("Updating Order.Status")
10✔
260
                if err := r.Status().Update(ctx, order); err != nil {
12✔
261
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to update status")
2✔
262
                }
2✔
263
        }
264

265
        return ctrl.Result{}, nil
22✔
266
}
267

268
func (r *OrderReconciler) hydrateArtifactWorkflow(daw *desiredAW) (*arcv1alpha1.ArtifactWorkflow, error) {
15✔
269
        params, err := dawToParameters(daw)
15✔
270
        if err != nil {
15✔
UNCOV
271
                return nil, err
×
UNCOV
272
        }
×
273

274
        // Next we create the ArtifactWorkflow instance
275
        aw := &arcv1alpha1.ArtifactWorkflow{
15✔
276
                ObjectMeta: daw.objectMeta,
15✔
277
                Spec: arcv1alpha1.ArtifactWorkflowSpec{
15✔
278
                        Type:         daw.artifact.Type,
15✔
279
                        Parameters:   params,
15✔
280
                        SrcSecretRef: daw.srcEndpoint.Spec.SecretRef,
15✔
281
                        DstSecretRef: daw.dstEndpoint.Spec.SecretRef,
15✔
282
                },
15✔
283
        }
15✔
284

15✔
285
        return aw, nil
15✔
286
}
287

288
// generateReconcileRequestsForEndpoint generates reconcile requests for all Endpoints referenced by an Order
289
func (r *OrderReconciler) generateReconcileRequestsForEndpoint(ctx context.Context, endpoint client.Object) []reconcile.Request {
23✔
290
        resourcesReferencingEndpoint := &arcv1alpha1.OrderList{}
23✔
291
        listOps := &client.ListOptions{
23✔
292
                FieldSelector: fields.SelectorFromSet(fields.Set{".spec.srcRef.name": endpoint.GetName(), ".spec.dstRef.name": endpoint.GetName()}),
23✔
293
                Namespace:     endpoint.GetNamespace(),
23✔
294
        }
23✔
295
        err := r.List(ctx, resourcesReferencingEndpoint, listOps)
23✔
296
        if err != nil {
46✔
297
                return []reconcile.Request{}
23✔
298
        }
23✔
299

UNCOV
300
        requests := make([]reconcile.Request, len(resourcesReferencingEndpoint.Items))
×
UNCOV
301
        for i, item := range resourcesReferencingEndpoint.Items {
×
UNCOV
302
                log := ctrl.LoggerFrom(ctx)
×
UNCOV
303
                log.V(1).Info("Generating reconcile request for resource because referenced endpoint has changed...")
×
UNCOV
304
                requests[i] = reconcile.Request{
×
UNCOV
305
                        NamespacedName: types.NamespacedName{
×
UNCOV
306
                                Name:      item.GetName(),
×
UNCOV
307
                                Namespace: item.GetNamespace(),
×
UNCOV
308
                        },
×
UNCOV
309
                }
×
UNCOV
310
        }
×
UNCOV
311
        return requests
×
312
}
313

314
// SetupWithManager sets up the controller with the Manager.
315
func (r *OrderReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
316
        return ctrl.NewControllerManagedBy(mgr).
1✔
317
                For(&arcv1alpha1.Order{}).
1✔
318
                Watches(
1✔
319
                        &arcv1alpha1.Endpoint{},
1✔
320
                        handler.EnqueueRequestsFromMapFunc(r.generateReconcileRequestsForEndpoint),
1✔
321
                        builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
1✔
322
                ).
1✔
323
                Owns(&arcv1alpha1.ArtifactWorkflow{}).
1✔
324
                Complete(r)
1✔
325
}
1✔
326

327
func namespacedName(namespace, name string) types.NamespacedName {
168✔
328
        return types.NamespacedName{
168✔
329
                Namespace: namespace,
168✔
330
                Name:      name,
168✔
331
        }
168✔
332
}
168✔
333

334
func awName(order *arcv1alpha1.Order, sha string) string {
47✔
335
        return fmt.Sprintf("%s-%s", order.Name, sha)
47✔
336
}
47✔
337

338
func awObjectMeta(order *arcv1alpha1.Order, sha string) metav1.ObjectMeta {
47✔
339
        return metav1.ObjectMeta{
47✔
340
                Namespace: order.Namespace,
47✔
341
                Name:      awName(order, sha),
47✔
342
        }
47✔
343
}
47✔
344

345
// TODO: add unit tests
346
func dawToParameters(daw *desiredAW) ([]arcv1alpha1.ArtifactWorkflowParameter, error) {
15✔
347
        params := []arcv1alpha1.ArtifactWorkflowParameter{
15✔
348
                {
15✔
349
                        Name:  paramName("src", "type"),
15✔
350
                        Value: string(daw.srcEndpoint.Spec.Type),
15✔
351
                },
15✔
352
                {
15✔
353
                        Name:  paramName("src", "remoteURL"),
15✔
354
                        Value: daw.srcEndpoint.Spec.RemoteURL,
15✔
355
                },
15✔
356
                {
15✔
357
                        Name:  paramName("dst", "type"),
15✔
358
                        Value: string(daw.dstEndpoint.Spec.Type),
15✔
359
                },
15✔
360
                {
15✔
361
                        Name:  paramName("dst", "remoteURL"),
15✔
362
                        Value: daw.dstEndpoint.Spec.RemoteURL,
15✔
363
                },
15✔
364
                {
15✔
365
                        Name:  "srcSecret",
15✔
366
                        Value: fmt.Sprintf("%v", daw.srcSecret.Name != ""),
15✔
367
                },
15✔
368
                {
15✔
369
                        Name:  "dstSecret",
15✔
370
                        Value: fmt.Sprintf("%v", daw.dstSecret.Name != ""),
15✔
371
                },
15✔
372
        }
15✔
373

15✔
374
        spec := map[string]any{}
15✔
375
        raw := daw.artifact.Spec.Raw
15✔
376
        if len(raw) == 0 {
21✔
377
                raw = []byte("{}")
6✔
378
        }
6✔
379
        if err := json.Unmarshal(raw, &spec); err != nil {
15✔
UNCOV
380
                return nil, err
×
UNCOV
381
        }
×
382
        flattened := map[string]any{}
15✔
383
        flattenMap("spec", spec, flattened)
15✔
384
        for name, value := range flattened {
24✔
385
                params = append(params, arcv1alpha1.ArtifactWorkflowParameter{
9✔
386
                        Name:  name,
9✔
387
                        Value: fmt.Sprintf("%v", value),
9✔
388
                })
9✔
389
        }
9✔
390

391
        return params, nil
15✔
392
}
393

394
// TODO: add unit tests
395
func paramName(prefix, suffix string) string {
60✔
396
        return prefix + strings.ToUpper(suffix[:1]) + suffix[1:]
60✔
397
}
60✔
398

399
// TODO: add unit tests
400
func flattenMap(prefix string, src map[string]any, dst map[string]any) {
15✔
401
        for k, v := range src {
24✔
402
                kt := strings.ToUpper(k[:1]) + k[1:]
9✔
403
                switch child := v.(type) {
9✔
UNCOV
404
                case map[string]any:
×
UNCOV
405
                        flattenMap(prefix+k, child, dst)
×
UNCOV
406
                case []any:
×
UNCOV
407
                        for i, av := range child {
×
UNCOV
408
                                dst[prefix+kt+strconv.Itoa(i)] = av
×
UNCOV
409
                        }
×
410
                default:
9✔
411
                        dst[prefix+kt] = v
9✔
412
                }
413
        }
414
}
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