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

opendefensecloud / artifact-conduit / 19627487675

24 Nov 2025 08:10AM UTC coverage: 63.293% (+1.6%) from 61.662%
19627487675

push

github

jastBytes
only update status if workflow is not in progress or phase changed

12 of 17 new or added lines in 1 file covered. (70.59%)

48 existing lines in 2 files now uncovered.

469 of 741 relevant lines covered (63.29%)

496.36 hits per line

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

80.63
/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
        "slices"
12

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

29
const (
30
        orderFinalizer = "arc.bwi.de/order-finalizer"
31
)
32

33
// OrderReconciler reconciles a Order object
34
type OrderReconciler struct {
35
        client.Client
36
        Scheme *runtime.Scheme
37
}
38

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

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

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

1,199✔
60
        // Fetch the Order instance
1,199✔
61
        order := &arcv1alpha1.Order{}
1,199✔
62
        if err := r.Get(ctx, req.NamespacedName, order); err != nil {
1,202✔
63
                if apierrors.IsNotFound(err) {
6✔
64
                        // Object not found, return. Created objects are automatically garbage collected.
3✔
65
                        return ctrl.Result{}, nil
3✔
66
                }
3✔
UNCOV
67
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get object")
×
68
        }
69

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

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

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

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

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

149
                dstSecret := &corev1.Secret{}
2,208✔
150
                if dstEndpoint.Spec.SecretRef.Name != "" {
4,414✔
151
                        if err := r.Get(ctx, namespacedName(order.Namespace, dstEndpoint.Spec.SecretRef.Name), dstSecret); err != nil {
2,206✔
UNCOV
152
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch secret for destination")
×
UNCOV
153
                        }
×
154
                }
155

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

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

186
        // List missing artifact workflows
187
        createAWs := []string{}
1,186✔
188
        for sha := range desiredAWs {
3,394✔
189
                _, exists := order.Status.ArtifactWorkflows[sha]
2,208✔
190
                if exists {
4,401✔
191
                        continue
2,193✔
192
                }
193
                createAWs = append(createAWs, sha)
15✔
194
        }
195

196
        // Make sure status is initialized
197
        if order.Status.ArtifactWorkflows == nil {
1,194✔
198
                order.Status.ArtifactWorkflows = map[string]arcv1alpha1.OrderArtifactWorkflowStatus{}
8✔
199
        }
8✔
200

201
        // Find obsolete artifact workflows
202
        deleteAWs := []string{}
1,186✔
203
        for sha := range order.Status.ArtifactWorkflows {
3,380✔
204
                _, exists := desiredAWs[sha]
2,194✔
205
                if exists {
4,387✔
206
                        continue
2,193✔
207
                }
208
                deleteAWs = append(deleteAWs, sha)
1✔
209
        }
210

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

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

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

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

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

249
                // Update status
250
                delete(order.Status.ArtifactWorkflows, sha)
1✔
251
        }
252

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

271
        // Update status
272
        if len(createAWs) > 0 || len(deleteAWs) > 0 || anyPhaseChanged {
1,953✔
273
                log.V(1).Info("Updating order status")
767✔
274
                // Make sure ArtifactIndex is up to date
767✔
275
                for sha, daw := range desiredAWs {
2,203✔
276
                        aws := order.Status.ArtifactWorkflows[sha]
1,436✔
277
                        aws.ArtifactIndex = daw.index
1,436✔
278
                        order.Status.ArtifactWorkflows[sha] = aws
1,436✔
279
                }
1,436✔
280
                if err := r.Status().Update(ctx, order); err != nil {
804✔
281
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to update status")
37✔
282
                }
37✔
283
        }
284

285
        return ctrl.Result{}, nil
1,149✔
286
}
287

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

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

15✔
305
        return aw, nil
15✔
306
}
307

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

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

334
// SetupWithManager sets up the controller with the Manager.
335
func (r *OrderReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
336
        return ctrl.NewControllerManagedBy(mgr).
1✔
337
                For(&arcv1alpha1.Order{}).
1✔
338
                Watches(
1✔
339
                        &arcv1alpha1.Endpoint{},
1✔
340
                        handler.EnqueueRequestsFromMapFunc(r.generateReconcileRequestsForEndpoint),
1✔
341
                        builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
1✔
342
                ).
1✔
343
                Owns(&arcv1alpha1.ArtifactWorkflow{}).
1✔
344
                Complete(r)
1✔
345
}
1✔
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