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

opendefensecloud / artifact-conduit / 19667009240

25 Nov 2025 10:52AM UTC coverage: 63.752% (-0.4%) from 64.132%
19667009240

push

github

jastBytes
fix docs

503 of 789 relevant lines covered (63.75%)

837.88 hits per line

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

78.99
/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

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

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

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

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

50
//+kubebuilder:rbac:groups=arc.bwi.de,resources=endpoints,verbs=get;list;watch
51
//+kubebuilder:rbac:groups=arc.bwi.de,resources=artifacttypes,verbs=get;list;watch
52
//+kubebuilder:rbac:groups=arc.bwi.de,resources=clusterartifacttypes,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) {
2,097✔
61
        log := ctrl.LoggerFrom(ctx)
2,097✔
62

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

73
        // Handle deletion: cleanup artifact workflows, then remove finalizer
74
        if !order.DeletionTimestamp.IsZero() {
2,097✔
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 artifact workflows are gone
1✔
90
                        return ctrl.Result{}, nil
1✔
91
                }
92
                // All artifact workflows 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() {
4,186✔
107
                if !slices.Contains(order.Finalizers, orderFinalizer) {
2,103✔
108
                        log.V(1).Info("Adding finalizer to Order")
10✔
109
                        order.Finalizers = append(order.Finalizers, orderFinalizer)
10✔
110
                        if err := r.Update(ctx, order); err != nil {
10✔
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
10✔
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{}
2,083✔
121
        for i, artifact := range order.Spec.Artifacts {
5,421✔
122
                // TODO: When a endpoint or secret fetch fails, we stop the reconciliation of the whole order.
3,338✔
123
                //       Should we instead not fail but skip invalid artifacts?
3,338✔
124
                log := log.WithValues("artifactIndex", i)
3,338✔
125

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

144
                // Validate that the endpoint usage is correct
145
                if srcEndpoint.Spec.Usage != arcv1alpha1.EndpointUsagePullOnly && srcEndpoint.Spec.Usage != arcv1alpha1.EndpointUsageAll {
3,338✔
146
                        err := fmt.Errorf("endpoint '%s' usage '%s' is not compatible with source usage", srcEndpoint.Name, srcEndpoint.Spec.Usage)
×
147
                        return ctrl.Result{}, errLogAndWrap(log, err, "artifact validation failed")
×
148
                }
×
149
                if dstEndpoint.Spec.Usage != arcv1alpha1.EndpointUsagePushOnly && dstEndpoint.Spec.Usage != arcv1alpha1.EndpointUsageAll {
3,338✔
150
                        err := fmt.Errorf("endpoint '%s' usage '%s' is not compatible with destination usage", dstEndpoint.Name, dstEndpoint.Spec.Usage)
×
151
                        return ctrl.Result{}, errLogAndWrap(log, err, "artifact validation failed")
×
152
                }
×
153

154
                // Validate against ArtifactType rules
155
                artifactType := &arcv1alpha1.ArtifactType{}
3,338✔
156
                if err := r.Get(ctx, namespacedName(order.Namespace, artifact.Type), artifactType); client.IgnoreNotFound(err) != nil {
3,338✔
157
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch referenced ArtifactType")
×
158
                }
×
159
                var (
3,338✔
160
                        artifactTypeGen  int64
3,338✔
161
                        artifactTypeSpec *arcv1alpha1.ArtifactTypeSpec
3,338✔
162
                )
3,338✔
163
                if artifactType.Name == "" { // was not found, let's check ClusterArtifactType
6,476✔
164
                        clusterArtifactType := &arcv1alpha1.ClusterArtifactType{}
3,138✔
165
                        if err := r.Get(ctx, namespacedName("", artifact.Type), clusterArtifactType); err != nil {
3,170✔
166
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch ArtifactType or ClusterArtifactType")
32✔
167
                        }
32✔
168
                        artifactTypeSpec = &clusterArtifactType.Spec
3,106✔
169
                        artifactTypeGen = clusterArtifactType.Generation
3,106✔
170
                } else {
200✔
171
                        artifactTypeSpec = &artifactType.Spec
200✔
172
                        artifactTypeGen = artifactType.Generation
200✔
173
                }
200✔
174

175
                if len(artifactTypeSpec.Rules.SrcTypes) > 0 && !slices.Contains(artifactTypeSpec.Rules.SrcTypes, srcEndpoint.Spec.Type) {
3,315✔
176
                        err := fmt.Errorf("source endpoint type '%s' is not allowed by ArtifactType rules", srcEndpoint.Spec.Type)
9✔
177
                        return ctrl.Result{}, errLogAndWrap(log, err, "artifact validation failed")
9✔
178
                }
9✔
179
                if len(artifactTypeSpec.Rules.DstTypes) > 0 && !slices.Contains(artifactTypeSpec.Rules.DstTypes, dstEndpoint.Spec.Type) {
3,297✔
180
                        err := fmt.Errorf("destination endpoint type '%s' is not allowed by ArtifactType rules", dstEndpoint.Spec.Type)
×
181
                        return ctrl.Result{}, errLogAndWrap(log, err, "artifact validation failed")
×
182
                }
×
183

184
                // Next, we need the secret contents
185
                srcSecret := &corev1.Secret{}
3,297✔
186
                if srcEndpoint.Spec.SecretRef.Name != "" {
6,592✔
187
                        if err := r.Get(ctx, namespacedName(order.Namespace, srcEndpoint.Spec.SecretRef.Name), srcSecret); err != nil {
3,295✔
188
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch secret for source")
×
189
                        }
×
190
                }
191

192
                dstSecret := &corev1.Secret{}
3,297✔
193
                if dstEndpoint.Spec.SecretRef.Name != "" {
6,592✔
194
                        if err := r.Get(ctx, namespacedName(order.Namespace, dstEndpoint.Spec.SecretRef.Name), dstSecret); err != nil {
3,295✔
195
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch secret for destination")
×
196
                        }
×
197
                }
198

199
                // Create a hash based on all related data for idempotency and compute the workflow name
200
                h := sha256.New()
3,297✔
201
                data := []any{
3,297✔
202
                        order.Namespace,
3,297✔
203
                        artifact.Type, artifact.Spec.Raw, artifactTypeGen,
3,297✔
204
                        srcEndpoint.Name, srcEndpoint.Generation,
3,297✔
205
                        dstEndpoint.Name, dstEndpoint.Generation,
3,297✔
206
                        srcSecret.Name, srcSecret.Generation,
3,297✔
207
                        dstSecret.Name, dstSecret.Generation,
3,297✔
208
                }
3,297✔
209
                jsonData, err := json.Marshal(data)
3,297✔
210
                if err != nil {
3,297✔
211
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to marshal artifact workflow data")
×
212
                }
×
213
                h.Write(jsonData)
3,297✔
214
                sha := hex.EncodeToString(h.Sum(nil))[:16]
3,297✔
215

3,297✔
216
                // We gave all the information to further process this artifact workflow.
3,297✔
217
                // Let's store it to compare it to the current status!
3,297✔
218
                desiredAWs[sha] = desiredAW{
3,297✔
219
                        index:       i,
3,297✔
220
                        objectMeta:  awObjectMeta(order, sha),
3,297✔
221
                        artifact:    &artifact,
3,297✔
222
                        srcEndpoint: srcEndpoint,
3,297✔
223
                        dstEndpoint: dstEndpoint,
3,297✔
224
                        srcSecret:   srcSecret,
3,297✔
225
                        dstSecret:   dstSecret,
3,297✔
226
                }
3,297✔
227
        }
228

229
        // List missing artifact workflows
230
        createAWs := []string{}
2,042✔
231
        for sha := range desiredAWs {
5,335✔
232
                _, exists := order.Status.ArtifactWorkflows[sha]
3,293✔
233
                if exists {
6,570✔
234
                        continue
3,277✔
235
                }
236
                createAWs = append(createAWs, sha)
16✔
237
        }
238

239
        // Make sure status is initialized
240
        if order.Status.ArtifactWorkflows == nil {
2,051✔
241
                order.Status.ArtifactWorkflows = map[string]arcv1alpha1.OrderArtifactWorkflowStatus{}
9✔
242
        }
9✔
243

244
        // Find obsolete artifact workflows
245
        deleteAWs := []string{}
2,042✔
246
        for sha := range order.Status.ArtifactWorkflows {
5,320✔
247
                _, exists := desiredAWs[sha]
3,278✔
248
                if exists {
6,555✔
249
                        continue
3,277✔
250
                }
251
                deleteAWs = append(deleteAWs, sha)
1✔
252
        }
253

254
        // Create missing artifact workflows
255
        for _, sha := range createAWs {
2,058✔
256
                daw := desiredAWs[sha]
16✔
257
                aw, err := r.hydrateArtifactWorkflow(&daw)
16✔
258
                if err != nil {
16✔
259
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to hydrate artifact workflow")
×
260
                }
×
261

262
                // Set owner references
263
                if err := controllerutil.SetControllerReference(order, aw, r.Scheme); err != nil {
16✔
264
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to set controller reference")
×
265
                }
×
266

267
                // Create artifact workflow
268
                if err := r.Create(ctx, aw); err != nil {
16✔
269
                        if apierrors.IsAlreadyExists(err) {
×
270
                                // Already created by a previous reconcile — that's fine
×
271
                                continue
×
272
                        }
273
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to create artifact workflow")
×
274
                }
275

276
                // Update status
277
                order.Status.ArtifactWorkflows[sha] = arcv1alpha1.OrderArtifactWorkflowStatus{
16✔
278
                        ArtifactIndex: daw.index,
16✔
279
                        Phase:         arcv1alpha1.WorkflowUnknown,
16✔
280
                }
16✔
281
        }
282

283
        // Delete obsolete artifact workflows
284
        for _, sha := range deleteAWs {
2,043✔
285
                // Does not exist anymore, let's clean up!
1✔
286
                if err := r.Delete(ctx, &arcv1alpha1.ArtifactWorkflow{
1✔
287
                        ObjectMeta: awObjectMeta(order, sha),
1✔
288
                }); client.IgnoreNotFound(err) != nil {
1✔
289
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete artifact workflow")
×
290
                }
×
291

292
                // Update status
293
                delete(order.Status.ArtifactWorkflows, sha)
1✔
294
        }
295

296
        anyPhaseChanged := false
2,042✔
297
        for sha, daw := range desiredAWs {
5,335✔
298
                if slices.Contains(createAWs, sha) {
3,309✔
299
                        // If it was just created we skip the update
16✔
300
                        continue
16✔
301
                }
302
                aw := arcv1alpha1.ArtifactWorkflow{}
3,277✔
303
                if err := r.Get(ctx, namespacedName(daw.objectMeta.Namespace, daw.objectMeta.Name), &aw); err != nil {
3,277✔
304
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get artifact workflow")
×
305
                }
×
306
                if order.Status.ArtifactWorkflows[sha].Phase != aw.Status.Phase {
4,616✔
307
                        awStatus := order.Status.ArtifactWorkflows[sha]
1,339✔
308
                        awStatus.Phase = aw.Status.Phase
1,339✔
309
                        order.Status.ArtifactWorkflows[sha] = awStatus
1,339✔
310
                        anyPhaseChanged = true
1,339✔
311
                }
1,339✔
312
        }
313

314
        // Update status
315
        if len(createAWs) > 0 || len(deleteAWs) > 0 || anyPhaseChanged {
3,345✔
316
                log.V(1).Info("Updating order status")
1,303✔
317
                // Make sure ArtifactIndex is up to date
1,303✔
318
                for sha, daw := range desiredAWs {
3,428✔
319
                        aws := order.Status.ArtifactWorkflows[sha]
2,125✔
320
                        aws.ArtifactIndex = daw.index
2,125✔
321
                        order.Status.ArtifactWorkflows[sha] = aws
2,125✔
322
                }
2,125✔
323
                if err := r.Status().Update(ctx, order); err != nil {
1,348✔
324
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to update status")
45✔
325
                }
45✔
326
        }
327

328
        return ctrl.Result{}, nil
1,997✔
329
}
330

331
func (r *OrderReconciler) hydrateArtifactWorkflow(daw *desiredAW) (*arcv1alpha1.ArtifactWorkflow, error) {
16✔
332
        params, err := dawToParameters(daw)
16✔
333
        if err != nil {
16✔
334
                return nil, err
×
335
        }
×
336

337
        // Next we create the ArtifactWorkflow instance
338
        aw := &arcv1alpha1.ArtifactWorkflow{
16✔
339
                ObjectMeta: daw.objectMeta,
16✔
340
                Spec: arcv1alpha1.ArtifactWorkflowSpec{
16✔
341
                        Type:         daw.artifact.Type,
16✔
342
                        Parameters:   params,
16✔
343
                        SrcSecretRef: daw.srcEndpoint.Spec.SecretRef,
16✔
344
                        DstSecretRef: daw.dstEndpoint.Spec.SecretRef,
16✔
345
                },
16✔
346
        }
16✔
347

16✔
348
        return aw, nil
16✔
349
}
350

351
// generateReconcileRequestsForEndpoint generates reconcile requests for all Endpoints referenced by an Order
352
func (r *OrderReconciler) generateReconcileRequestsForEndpoint(ctx context.Context, endpoint client.Object) []reconcile.Request {
29✔
353
        resourcesReferencingEndpoint := &arcv1alpha1.OrderList{}
29✔
354
        listOps := &client.ListOptions{
29✔
355
                FieldSelector: fields.SelectorFromSet(fields.Set{".spec.srcRef.name": endpoint.GetName(), ".spec.dstRef.name": endpoint.GetName()}),
29✔
356
                Namespace:     endpoint.GetNamespace(),
29✔
357
        }
29✔
358
        err := r.List(ctx, resourcesReferencingEndpoint, listOps)
29✔
359
        if err != nil {
58✔
360
                return []reconcile.Request{}
29✔
361
        }
29✔
362

363
        requests := make([]reconcile.Request, len(resourcesReferencingEndpoint.Items))
×
364
        for i, item := range resourcesReferencingEndpoint.Items {
×
365
                log := ctrl.LoggerFrom(ctx)
×
366
                log.V(1).Info("Generating reconcile request for resource because referenced endpoint has changed...")
×
367
                requests[i] = reconcile.Request{
×
368
                        NamespacedName: types.NamespacedName{
×
369
                                Name:      item.GetName(),
×
370
                                Namespace: item.GetNamespace(),
×
371
                        },
×
372
                }
×
373
        }
×
374
        return requests
×
375
}
376

377
// SetupWithManager sets up the controller with the Manager.
378
func (r *OrderReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
379
        return ctrl.NewControllerManagedBy(mgr).
1✔
380
                For(&arcv1alpha1.Order{}).
1✔
381
                Watches(
1✔
382
                        &arcv1alpha1.Endpoint{},
1✔
383
                        handler.EnqueueRequestsFromMapFunc(r.generateReconcileRequestsForEndpoint),
1✔
384
                        builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
1✔
385
                ).
1✔
386
                Owns(&arcv1alpha1.ArtifactWorkflow{}).
1✔
387
                Complete(r)
1✔
388
}
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