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

opendefensecloud / artifact-conduit / 19664486947

25 Nov 2025 09:21AM UTC coverage: 63.878% (+0.3%) from 63.565%
19664486947

push

github

web-flow
Feature/cluster artifact type (#80)

Closes #52

44 of 47 new or added lines in 3 files covered. (93.62%)

4 existing lines in 2 files now uncovered.

504 of 789 relevant lines covered (63.88%)

751.29 hits per line

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

80.16
/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) {
1,874✔
61
        log := ctrl.LoggerFrom(ctx)
1,874✔
62

1,874✔
63
        // Fetch the Order instance
1,874✔
64
        order := &arcv1alpha1.Order{}
1,874✔
65
        if err := r.Get(ctx, req.NamespacedName, order); err != nil {
1,876✔
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() {
1,874✔
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() {
3,740✔
107
                if !slices.Contains(order.Finalizers, orderFinalizer) {
1,880✔
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{}
1,860✔
121
        for i, artifact := range order.Spec.Artifacts {
4,783✔
122
                // TODO: When a endpoint or secret fetch fails, we stop the reconciliation of the whole order.
2,923✔
123
                //       Should we instead not fail but skip invalid artifacts?
2,923✔
124
                log := log.WithValues("artifactIndex", i)
2,923✔
125

2,923✔
126
                // We need the referenced src- and dst-endpoints for the artifact
2,923✔
127
                srcRefName := artifact.SrcRef.Name
2,923✔
128
                if srcRefName == "" {
3,451✔
129
                        srcRefName = order.Spec.Defaults.SrcRef.Name
528✔
130
                }
528✔
131
                dstRefName := artifact.DstRef.Name
2,923✔
132
                if dstRefName == "" {
3,628✔
133
                        dstRefName = order.Spec.Defaults.DstRef.Name
705✔
134
                }
705✔
135
                srcEndpoint := &arcv1alpha1.Endpoint{}
2,923✔
136
                if err := r.Get(ctx, namespacedName(order.Namespace, srcRefName), srcEndpoint); err != nil {
2,923✔
137
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch endpoint for source")
×
138
                }
×
139
                dstEndpoint := &arcv1alpha1.Endpoint{}
2,923✔
140
                if err := r.Get(ctx, namespacedName(order.Namespace, dstRefName), dstEndpoint); err != nil {
2,923✔
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 {
2,923✔
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 {
2,923✔
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{}
2,923✔
156
                if err := r.Get(ctx, namespacedName(order.Namespace, artifact.Type), artifactType); client.IgnoreNotFound(err) != nil {
2,923✔
UNCOV
157
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch referenced ArtifactType")
×
UNCOV
158
                }
×
159
                var (
2,923✔
160
                        artifactTypeGen  int64
2,923✔
161
                        artifactTypeSpec *arcv1alpha1.ArtifactTypeSpec
2,923✔
162
                )
2,923✔
163
                if artifactType.Name == "" { // was not found, let's check ClusterArtifactType
5,652✔
164
                        clusterArtifactType := &arcv1alpha1.ClusterArtifactType{}
2,729✔
165
                        if err := r.Get(ctx, namespacedName("", artifact.Type), clusterArtifactType); err != nil {
2,753✔
166
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch ArtifactType or ClusterArtifactType")
24✔
167
                        }
24✔
168
                        artifactTypeSpec = &clusterArtifactType.Spec
2,705✔
169
                        artifactTypeGen = clusterArtifactType.Generation
2,705✔
170
                } else {
194✔
171
                        artifactTypeSpec = &artifactType.Spec
194✔
172
                        artifactTypeGen = artifactType.Generation
194✔
173
                }
194✔
174

175
                if len(artifactTypeSpec.Rules.SrcTypes) > 0 && !slices.Contains(artifactTypeSpec.Rules.SrcTypes, srcEndpoint.Spec.Type) {
2,908✔
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) {
2,890✔
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{}
2,890✔
186
                if srcEndpoint.Spec.SecretRef.Name != "" {
5,588✔
187
                        if err := r.Get(ctx, namespacedName(order.Namespace, srcEndpoint.Spec.SecretRef.Name), srcSecret); err != nil {
2,698✔
188
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to fetch secret for source")
×
189
                        }
×
190
                }
191

192
                dstSecret := &corev1.Secret{}
2,890✔
193
                if dstEndpoint.Spec.SecretRef.Name != "" {
5,588✔
194
                        if err := r.Get(ctx, namespacedName(order.Namespace, dstEndpoint.Spec.SecretRef.Name), dstSecret); err != nil {
2,698✔
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()
2,890✔
201
                data := []any{
2,890✔
202
                        order.Namespace,
2,890✔
203
                        artifact.Type, artifact.Spec.Raw, artifactTypeGen,
2,890✔
204
                        srcEndpoint.Name, srcEndpoint.Generation,
2,890✔
205
                        dstEndpoint.Name, dstEndpoint.Generation,
2,890✔
206
                        srcSecret.Name, srcSecret.Generation,
2,890✔
207
                        dstSecret.Name, dstSecret.Generation,
2,890✔
208
                }
2,890✔
209
                jsonData, err := json.Marshal(data)
2,890✔
210
                if err != nil {
2,890✔
211
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to marshal artifact workflow data")
×
212
                }
×
213
                h.Write(jsonData)
2,890✔
214
                sha := hex.EncodeToString(h.Sum(nil))[:16]
2,890✔
215

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

229
        // List missing artifact workflows
230
        createAWs := []string{}
1,827✔
231
        for sha := range desiredAWs {
4,712✔
232
                _, exists := order.Status.ArtifactWorkflows[sha]
2,885✔
233
                if exists {
5,752✔
234
                        continue
2,867✔
235
                }
236
                createAWs = append(createAWs, sha)
18✔
237
        }
238

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

244
        // Find obsolete artifact workflows
245
        deleteAWs := []string{}
1,827✔
246
        for sha := range order.Status.ArtifactWorkflows {
4,696✔
247
                _, exists := desiredAWs[sha]
2,869✔
248
                if exists {
5,736✔
249
                        continue
2,867✔
250
                }
251
                deleteAWs = append(deleteAWs, sha)
2✔
252
        }
253

254
        // Create missing artifact workflows
255
        for _, sha := range createAWs {
1,845✔
256
                daw := desiredAWs[sha]
18✔
257
                aw, err := r.hydrateArtifactWorkflow(&daw)
18✔
258
                if err != nil {
18✔
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 {
18✔
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 {
20✔
269
                        if apierrors.IsAlreadyExists(err) {
4✔
270
                                // Already created by a previous reconcile — that's fine
2✔
271
                                continue
2✔
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 {
1,829✔
285
                // Does not exist anymore, let's clean up!
2✔
286
                if err := r.Delete(ctx, &arcv1alpha1.ArtifactWorkflow{
2✔
287
                        ObjectMeta: awObjectMeta(order, sha),
2✔
288
                }); client.IgnoreNotFound(err) != nil {
2✔
289
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete artifact workflow")
×
290
                }
×
291

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

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

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

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

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

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

18✔
348
        return aw, nil
18✔
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