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

opendefensecloud / artifact-conduit / 19715464629

26 Nov 2025 07:38PM UTC coverage: 61.735% (+1.1%) from 60.656%
19715464629

push

github

web-flow
Add LastReconcileAt and LastForceAt fields with force reconcile functionality (#95)

Introduce LastReconcileAt and LastForceAt fields to track reconciliation
times in artifact and order statuses. Implement force reconcile
functionality for orders and workflows, along with tests to ensure
proper handling of the force reconcile annotation.

Closes #58.

81 of 115 new or added lines in 3 files covered. (70.43%)

3 existing lines in 2 files now uncovered.

605 of 980 relevant lines covered (61.73%)

904.23 hits per line

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

80.13
/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
        "time"
14

15
        "github.com/go-logr/logr"
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/runtime"
21
        "k8s.io/client-go/tools/record"
22
        ctrl "sigs.k8s.io/controller-runtime"
23
        "sigs.k8s.io/controller-runtime/pkg/client"
24
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
25
)
26

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

31
// OrderReconciler reconciles a Order object
32
type OrderReconciler struct {
33
        client.Client
34
        Scheme   *runtime.Scheme
35
        Recorder record.EventRecorder
36
}
37

38
type desiredAW struct {
39
        index       int
40
        objectMeta  metav1.ObjectMeta
41
        artifact    *arcv1alpha1.OrderArtifact
42
        typeSpec    *arcv1alpha1.ArtifactTypeSpec
43
        srcEndpoint *arcv1alpha1.Endpoint
44
        dstEndpoint *arcv1alpha1.Endpoint
45
        srcSecret   *corev1.Secret
46
        dstSecret   *corev1.Secret
47
        sha         string
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
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
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) {
2,385✔
62
        log := ctrl.LoggerFrom(ctx)
2,385✔
63
        ctrlResult := ctrl.Result{}
2,385✔
64

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

75
        // Update last reconcile time
76
        order.Status.LastReconcileAt = metav1.Now()
2,382✔
77

2,382✔
78
        // Handle deletion: cleanup artifact workflows, then remove finalizer
2,382✔
79
        if !order.DeletionTimestamp.IsZero() {
2,384✔
80
                log.V(1).Info("Order is being deleted")
2✔
81
                r.Recorder.Event(order, corev1.EventTypeWarning, "Deleting", "Order is being deleted, cleaning up artifact workflows")
2✔
82

2✔
83
                // Cleanup all artifact workflows
2✔
84
                if len(order.Status.ArtifactWorkflows) > 0 {
3✔
85
                        for sha := range order.Status.ArtifactWorkflows {
3✔
86
                                // Remove ArtifactWorkflow
2✔
87
                                aw := &arcv1alpha1.ArtifactWorkflow{
2✔
88
                                        ObjectMeta: awObjectMeta(order, sha),
2✔
89
                                }
2✔
90
                                _ = r.Delete(ctx, aw) // Ignore errors
2✔
91
                                delete(order.Status.ArtifactWorkflows, sha)
2✔
92
                        }
2✔
93
                        if err := r.Status().Update(ctx, order); err != nil {
1✔
NEW
94
                                return ctrlResult, errLogAndWrap(log, err, "failed to update order status")
×
95
                        }
×
96
                        log.V(1).Info("Order artifact workflows cleaned up")
1✔
97

1✔
98
                        // Requeue until all artifact workflows are gone
1✔
99
                        return ctrlResult, nil
1✔
100
                }
101
                // All artifact workflows are gone, remove finalizer
102
                if slices.Contains(order.Finalizers, orderFinalizer) {
2✔
103
                        log.V(1).Info("No artifact workflows, removing finalizer from Order")
1✔
104
                        order.Finalizers = slices.DeleteFunc(order.Finalizers, func(f string) bool {
2✔
105
                                return f == orderFinalizer
1✔
106
                        })
1✔
107
                        if err := r.Update(ctx, order); err != nil {
1✔
NEW
108
                                return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer")
×
109
                        }
×
110
                }
111
                return ctrlResult, nil
1✔
112
        }
113

114
        // Add finalizer if not present and not deleting
115
        if order.DeletionTimestamp.IsZero() {
4,760✔
116
                if !slices.Contains(order.Finalizers, orderFinalizer) {
2,392✔
117
                        log.V(1).Info("Adding finalizer to Order")
12✔
118
                        order.Finalizers = append(order.Finalizers, orderFinalizer)
12✔
119
                        if err := r.Update(ctx, order); err != nil {
12✔
NEW
120
                                return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer")
×
121
                        }
×
122
                        // Return without requeue; the Update event will trigger reconciliation again
123
                        return ctrlResult, nil
12✔
124
                }
125
        }
126

127
        // Handle force reconcile annotation
128
        forceAt, err := GetForceAtAnnotationValue(order)
2,368✔
129
        if err != nil {
2,368✔
NEW
130
                log.V(1).Error(err, "Invalid force reconcile annotation, ignoring")
×
NEW
131
        }
×
132
        if !forceAt.IsZero() && (order.Status.LastForceAt.IsZero() || forceAt.After(order.Status.LastForceAt.Time)) {
2,369✔
133
                log.V(1).Info("Force reconcile requested")
1✔
134
                r.Recorder.Event(order, corev1.EventTypeNormal, "ForceReconcile", "Force reconcile requested via annotation")
1✔
135
                // Delete existing artifact workflows to force re-creation
1✔
136
                for sha := range order.Status.ArtifactWorkflows {
2✔
137
                        // Remove Secret and ArtifactWorkflow
1✔
138
                        aw := &arcv1alpha1.ArtifactWorkflow{
1✔
139
                                ObjectMeta: awObjectMeta(order, sha),
1✔
140
                        }
1✔
141
                        _ = r.Delete(ctx, aw) // Ignore errors
1✔
142
                        delete(order.Status.ArtifactWorkflows, sha)
1✔
143
                }
1✔
144
                // Update last force time
145
                order.Status.LastForceAt = metav1.Now()
1✔
146
                if err := r.Status().Update(ctx, order); err != nil {
1✔
NEW
147
                        return ctrlResult, errLogAndWrap(log, err, "failed to update last force time")
×
UNCOV
148
                }
×
149
                // Return without requeue; the update event will trigger reconciliation again
150
                return ctrlResult, nil
1✔
151
        }
152

153
        // Make sure status is initialized
154
        if order.Status.ArtifactWorkflows == nil {
2,391✔
155
                order.Status.ArtifactWorkflows = map[string]arcv1alpha1.OrderArtifactWorkflowStatus{}
24✔
156
        }
24✔
157

158
        // Before we compare to our status, let's fetch all necessary information
159
        // to compute desired state:
160
        desiredAWs := map[string]desiredAW{}
2,367✔
161
        for i, artifact := range order.Spec.Artifacts {
6,310✔
162
                daw, err := r.computeDesiredAW(ctx, log, order, &artifact, i)
3,943✔
163
                if err != nil {
3,982✔
164
                        r.Recorder.Event(order, corev1.EventTypeWarning, "ComputationFailed", fmt.Sprintf("Failed to compute desired artifact workflow for artifact index %d: %v", i, err))
39✔
165
                        order.Status.Message = fmt.Sprintf("Failed to compute desired artifact workflow for artifact index %d: %v", i, err)
39✔
166
                        if err := r.Status().Update(ctx, order); err != nil {
40✔
167
                                return ctrlResult, errLogAndWrap(log, err, "failed to update status")
1✔
168
                        }
1✔
169
                        return ctrlResult, errLogAndWrap(log, err, "failed to compute desired artifact workflow")
38✔
170
                }
171
                desiredAWs[daw.sha] = *daw
3,904✔
172
        }
173

174
        // List missing artifact workflows
175
        createAWs := []string{}
2,328✔
176
        for sha := range desiredAWs {
6,229✔
177
                _, exists := order.Status.ArtifactWorkflows[sha]
3,901✔
178
                if exists {
7,782✔
179
                        continue
3,881✔
180
                }
181
                createAWs = append(createAWs, sha)
20✔
182
        }
183

184
        // Find obsolete artifact workflows
185
        deleteAWs := []string{}
2,328✔
186
        for sha := range order.Status.ArtifactWorkflows {
6,210✔
187
                _, exists := desiredAWs[sha]
3,882✔
188
                if exists {
7,763✔
189
                        continue
3,881✔
190
                }
191
                deleteAWs = append(deleteAWs, sha)
1✔
192
        }
193

194
        // Find finished artifact workflows to clean up
195
        finishedAWs := []string{}
2,328✔
196
        for sha := range order.Status.ArtifactWorkflows {
6,210✔
197
                awStatus := order.Status.ArtifactWorkflows[sha]
3,882✔
198

3,882✔
199
                // Only consider succeeded workflows for TTL cleanup
3,882✔
200
                if awStatus.Phase != arcv1alpha1.WorkflowSucceeded {
7,656✔
201
                        continue
3,774✔
202
                }
203

204
                // If TTL is set, check if it has expired
205
                if order.Spec.TTLSecondsAfterCompletion != nil && *order.Spec.TTLSecondsAfterCompletion > 0 {
108✔
206
                        if time.Since(awStatus.CompletionTime.Time) > time.Duration(*order.Spec.TTLSecondsAfterCompletion)*time.Second {
×
207
                                finishedAWs = append(finishedAWs, sha)
×
NEW
208
                        } else {
×
NEW
209
                                // Requeue when the next TTL expires
×
NEW
210
                                ctrlResult.RequeueAfter = time.Duration((*order.Spec.TTLSecondsAfterCompletion)+1)*time.Second - time.Since(awStatus.CompletionTime.Time)
×
211
                        }
×
212
                        continue
×
213
                }
214

215
                // No TTL set, cleanup immediately
216
                finishedAWs = append(finishedAWs, sha)
108✔
217
        }
218

219
        // Create missing artifact workflows
220
        for _, sha := range createAWs {
2,348✔
221
                daw := desiredAWs[sha]
20✔
222
                aw, err := r.hydrateArtifactWorkflow(&daw)
20✔
223
                if err != nil {
20✔
224
                        r.Recorder.Event(order, corev1.EventTypeWarning, "HydrationFailed", fmt.Sprintf("Failed to hydrate artifact workflow for artifact index %d: %v", daw.index, err))
×
NEW
225
                        return ctrlResult, errLogAndWrap(log, err, "failed to hydrate artifact workflow")
×
226
                }
×
227

228
                // Set owner references
229
                if err := controllerutil.SetControllerReference(order, aw, r.Scheme); err != nil {
20✔
230
                        r.Recorder.Event(order, corev1.EventTypeWarning, "HydrationFailed", fmt.Sprintf("Failed to set controller reference for artifact workflow: %v", err))
×
NEW
231
                        return ctrlResult, errLogAndWrap(log, err, "failed to set controller reference")
×
232
                }
×
233

234
                // Create artifact workflow
235
                if err := r.Create(ctx, aw); err != nil {
21✔
236
                        if apierrors.IsAlreadyExists(err) {
2✔
237
                                // Already created by a previous reconcile — that's fine
1✔
238
                                continue
1✔
239
                        }
UNCOV
240
                        r.Recorder.Event(order, corev1.EventTypeWarning, "CreationFailed", fmt.Sprintf("Failed to create artifact workflow for artifact index %d: %v", daw.index, err))
×
NEW
241
                        return ctrlResult, errLogAndWrap(log, err, "failed to create artifact workflow")
×
242
                }
243

244
                // Update status
245
                order.Status.ArtifactWorkflows[sha] = arcv1alpha1.OrderArtifactWorkflowStatus{
19✔
246
                        ArtifactIndex: daw.index,
19✔
247
                        Phase:         arcv1alpha1.WorkflowUnknown,
19✔
248
                }
19✔
249

19✔
250
                r.Recorder.Event(order, corev1.EventTypeNormal, "ArtifactWorkflowCreated", fmt.Sprintf("Created artifact workflow '%s' for artifact index %d", aw.Name, daw.index))
19✔
251
                log.V(1).Info("Created artifact workflow", "artifactWorkflow", aw.Name)
19✔
252
        }
253

254
        // Delete obsolete artifact workflows
255
        for _, sha := range deleteAWs {
2,329✔
256
                // Does not exist anymore, let's clean up!
1✔
257
                if err := r.Delete(ctx, &arcv1alpha1.ArtifactWorkflow{
1✔
258
                        ObjectMeta: awObjectMeta(order, sha),
1✔
259
                }); client.IgnoreNotFound(err) != nil {
1✔
260
                        r.Recorder.Event(order, corev1.EventTypeWarning, "DeletionFailed", fmt.Sprintf("Failed to delete obsolete artifact workflow '%s': %v", sha, err))
×
NEW
261
                        return ctrlResult, errLogAndWrap(log, err, "failed to delete artifact workflow")
×
262
                }
×
263

264
                // Update status
265
                delete(order.Status.ArtifactWorkflows, sha)
1✔
266
                log.V(1).Info("Deleted obsolete artifact workflow", "artifactWorkflow", sha)
1✔
267
                r.Recorder.Event(order, corev1.EventTypeNormal, "ArtifactWorkflowDeleted", fmt.Sprintf("Deleted obsolete artifact workflow '%s'", sha))
1✔
268
        }
269

270
        // Delete finished artifact workflows
271
        for _, sha := range finishedAWs {
2,436✔
272
                // Finished, let's clean up!
108✔
273
                if err := r.Delete(ctx, &arcv1alpha1.ArtifactWorkflow{
108✔
274
                        ObjectMeta: awObjectMeta(order, sha),
108✔
275
                }); client.IgnoreNotFound(err) != nil {
108✔
276
                        r.Recorder.Event(order, corev1.EventTypeWarning, "DeletionFailed", fmt.Sprintf("Failed to delete finished artifact workflow '%s': %v", sha, err))
×
NEW
277
                        return ctrlResult, errLogAndWrap(log, err, "failed to delete artifact workflow")
×
278
                }
×
279

280
                log.V(1).Info("Deleted finished artifact workflow", "artifactWorkflow", sha)
108✔
281
                r.Recorder.Event(order, corev1.EventTypeNormal, "ArtifactWorkflowDeleted", fmt.Sprintf("Deleted finished artifact workflow '%s'", sha))
108✔
282
        }
283

284
        anyPhaseChanged := false
2,328✔
285
        for sha, daw := range desiredAWs {
6,229✔
286
                if slices.Contains(createAWs, sha) {
3,921✔
287
                        // If it was just created we skip the update
20✔
288
                        continue
20✔
289
                }
290
                if order.Status.ArtifactWorkflows[sha].Phase.Completed() {
3,989✔
291
                        // We do not need to check for updates if the workflow is completed
108✔
292
                        continue
108✔
293
                }
294
                aw := arcv1alpha1.ArtifactWorkflow{}
3,773✔
295
                if err := r.Get(ctx, namespacedName(daw.objectMeta.Namespace, daw.objectMeta.Name), &aw); err != nil {
3,780✔
296
                        return ctrlResult, errLogAndWrap(log, err, "failed to get artifact workflow")
7✔
297
                }
7✔
298
                if order.Status.ArtifactWorkflows[sha].Phase != aw.Status.Phase {
5,258✔
299
                        awStatus := order.Status.ArtifactWorkflows[sha]
1,492✔
300
                        awStatus.Phase = aw.Status.Phase
1,492✔
301
                        awStatus.CompletionTime = aw.Status.CompletionTime
1,492✔
302
                        order.Status.ArtifactWorkflows[sha] = awStatus
1,492✔
303
                        anyPhaseChanged = true
1,492✔
304
                }
1,492✔
305
        }
306

307
        // Update status
308
        if len(createAWs) > 0 || len(deleteAWs) > 0 || anyPhaseChanged {
3,782✔
309
                log.V(1).Info("Updating order status")
1,461✔
310
                // Make sure ArtifactIndex is up to date
1,461✔
311
                for sha, daw := range desiredAWs {
3,933✔
312
                        aws := order.Status.ArtifactWorkflows[sha]
2,472✔
313
                        aws.ArtifactIndex = daw.index
2,472✔
314
                        order.Status.ArtifactWorkflows[sha] = aws
2,472✔
315
                }
2,472✔
316
                if err := r.Status().Update(ctx, order); err != nil {
1,510✔
317
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
49✔
318
                }
49✔
319
        }
320

321
        return ctrlResult, nil
2,272✔
322
}
323

324
func (r *OrderReconciler) hydrateArtifactWorkflow(daw *desiredAW) (*arcv1alpha1.ArtifactWorkflow, error) {
20✔
325
        params, err := dawToParameters(daw)
20✔
326
        if err != nil {
20✔
327
                return nil, err
×
328
        }
×
329

330
        // Next we create the ArtifactWorkflow instance
331
        aw := &arcv1alpha1.ArtifactWorkflow{
20✔
332
                ObjectMeta: daw.objectMeta,
20✔
333
                Spec: arcv1alpha1.ArtifactWorkflowSpec{
20✔
334
                        WorkflowTemplateRef: daw.typeSpec.WorkflowTemplateRef,
20✔
335
                        Parameters:          params,
20✔
336
                        SrcSecretRef:        daw.srcEndpoint.Spec.SecretRef,
20✔
337
                        DstSecretRef:        daw.dstEndpoint.Spec.SecretRef,
20✔
338
                },
20✔
339
        }
20✔
340

20✔
341
        return aw, nil
20✔
342
}
343

344
func (r *OrderReconciler) computeDesiredAW(ctx context.Context, log logr.Logger, order *arcv1alpha1.Order, artifact *arcv1alpha1.OrderArtifact, i int) (*desiredAW, error) {
3,943✔
345
        log = log.WithValues("artifactIndex", i)
3,943✔
346

3,943✔
347
        // We need the referenced src- and dst-endpoints for the artifact
3,943✔
348
        srcRefName := artifact.SrcRef.Name
3,943✔
349
        if srcRefName == "" {
4,490✔
350
                srcRefName = order.Spec.Defaults.SrcRef.Name
547✔
351
        }
547✔
352
        dstRefName := artifact.DstRef.Name
3,943✔
353
        if dstRefName == "" {
4,666✔
354
                dstRefName = order.Spec.Defaults.DstRef.Name
723✔
355
        }
723✔
356
        srcEndpoint := &arcv1alpha1.Endpoint{}
3,943✔
357
        if err := r.Get(ctx, namespacedName(order.Namespace, srcRefName), srcEndpoint); err != nil {
3,943✔
358
                r.Recorder.Event(order, corev1.EventTypeWarning, "InvalidEndpoint", fmt.Sprintf("Failed to fetch source endpoint '%s': %v", srcRefName, err))
×
359
                return nil, errLogAndWrap(log, err, "failed to fetch endpoint for source")
×
360
        }
×
361
        dstEndpoint := &arcv1alpha1.Endpoint{}
3,943✔
362
        if err := r.Get(ctx, namespacedName(order.Namespace, dstRefName), dstEndpoint); err != nil {
3,943✔
363
                r.Recorder.Event(order, corev1.EventTypeWarning, "InvalidEndpoint", fmt.Sprintf("Failed to fetch destination endpoint '%s': %v", dstRefName, err))
×
364
                return nil, errLogAndWrap(log, err, "failed to fetch endpoint for destination")
×
365
        }
×
366

367
        // Validate that the endpoint usage is correct
368
        if srcEndpoint.Spec.Usage != arcv1alpha1.EndpointUsagePullOnly && srcEndpoint.Spec.Usage != arcv1alpha1.EndpointUsageAll {
3,943✔
369
                err := fmt.Errorf("endpoint '%s' usage '%s' is not compatible with source usage", srcEndpoint.Name, srcEndpoint.Spec.Usage)
×
370
                r.Recorder.Event(order, corev1.EventTypeWarning, "InvalidEndpoint", fmt.Sprintf("Source endpoint '%s' has incompatible usage '%s'", srcEndpoint.Name, srcEndpoint.Spec.Usage))
×
371
                return nil, errLogAndWrap(log, err, "artifact validation failed")
×
372
        }
×
373
        if dstEndpoint.Spec.Usage != arcv1alpha1.EndpointUsagePushOnly && dstEndpoint.Spec.Usage != arcv1alpha1.EndpointUsageAll {
3,943✔
374
                err := fmt.Errorf("endpoint '%s' usage '%s' is not compatible with destination usage", dstEndpoint.Name, dstEndpoint.Spec.Usage)
×
375
                r.Recorder.Event(order, corev1.EventTypeWarning, "InvalidEndpoint", fmt.Sprintf("Destination endpoint '%s' has incompatible usage '%s'", dstEndpoint.Name, dstEndpoint.Spec.Usage))
×
376
                return nil, errLogAndWrap(log, err, "artifact validation failed")
×
377
        }
×
378

379
        // Validate against ArtifactType rules
380
        artifactType := &arcv1alpha1.ArtifactType{}
3,943✔
381
        if err := r.Get(ctx, namespacedName(order.Namespace, artifact.Type), artifactType); client.IgnoreNotFound(err) != nil {
3,943✔
382
                r.Recorder.Event(order, corev1.EventTypeWarning, "InvalidArtifactType", fmt.Sprintf("Failed to fetch ArtifactType '%s': %v", artifact.Type, err))
×
383
                return nil, errLogAndWrap(log, err, "failed to fetch referenced ArtifactType")
×
384
        }
×
385
        var (
3,943✔
386
                artifactTypeGen  int64
3,943✔
387
                artifactTypeSpec *arcv1alpha1.ArtifactTypeSpec
3,943✔
388
        )
3,943✔
389
        if artifactType.Name == "" { // was not found, let's check ClusterArtifactType
7,708✔
390
                clusterArtifactType := &arcv1alpha1.ClusterArtifactType{}
3,765✔
391
                if err := r.Get(ctx, namespacedName("", artifact.Type), clusterArtifactType); err != nil {
3,794✔
392
                        return nil, errLogAndWrap(log, err, "failed to fetch ArtifactType or ClusterArtifactType")
29✔
393
                }
29✔
394
                artifactTypeSpec = &clusterArtifactType.Spec
3,736✔
395
                artifactTypeGen = clusterArtifactType.Generation
3,736✔
396
                // NOTE: ClusterArtifactTypes can only referes ClusterWorkflowTemplates, so we enforce this here:
3,736✔
397
                artifactTypeSpec.WorkflowTemplateRef.ClusterScope = true
3,736✔
398
        } else {
178✔
399
                artifactTypeSpec = &artifactType.Spec
178✔
400
                artifactTypeGen = artifactType.Generation
178✔
401
        }
178✔
402

403
        if len(artifactTypeSpec.Rules.SrcTypes) > 0 && !slices.Contains(artifactTypeSpec.Rules.SrcTypes, srcEndpoint.Spec.Type) {
3,924✔
404
                err := fmt.Errorf("source endpoint type '%s' is not allowed by ArtifactType rules", srcEndpoint.Spec.Type)
10✔
405
                r.Recorder.Event(order, corev1.EventTypeWarning, "InvalidArtifactType", fmt.Sprintf("Source endpoint type '%s' is not allowed by ArtifactType '%s' rules", srcEndpoint.Spec.Type, artifact.Type))
10✔
406
                return nil, errLogAndWrap(log, err, "artifact validation failed")
10✔
407
        }
10✔
408
        if len(artifactTypeSpec.Rules.DstTypes) > 0 && !slices.Contains(artifactTypeSpec.Rules.DstTypes, dstEndpoint.Spec.Type) {
3,904✔
409
                err := fmt.Errorf("destination endpoint type '%s' is not allowed by ArtifactType rules", dstEndpoint.Spec.Type)
×
410
                r.Recorder.Event(order, corev1.EventTypeWarning, "InvalidArtifactType", fmt.Sprintf("Destination endpoint type '%s' is not allowed by ArtifactType '%s' rules", dstEndpoint.Spec.Type, artifact.Type))
×
411
                return nil, errLogAndWrap(log, err, "artifact validation failed")
×
412
        }
×
413

414
        // Next, we need the secret contents
415
        srcSecret := &corev1.Secret{}
3,904✔
416
        if srcEndpoint.Spec.SecretRef.Name != "" {
7,630✔
417
                if err := r.Get(ctx, namespacedName(order.Namespace, srcEndpoint.Spec.SecretRef.Name), srcSecret); err != nil {
3,726✔
418
                        r.Recorder.Event(order, corev1.EventTypeWarning, "InvalidSecret", fmt.Sprintf("Failed to fetch source secret '%s': %v", srcEndpoint.Spec.SecretRef.Name, err))
×
419
                        return nil, errLogAndWrap(log, err, "failed to fetch secret for source")
×
420
                }
×
421
        }
422

423
        dstSecret := &corev1.Secret{}
3,904✔
424
        if dstEndpoint.Spec.SecretRef.Name != "" {
7,630✔
425
                if err := r.Get(ctx, namespacedName(order.Namespace, dstEndpoint.Spec.SecretRef.Name), dstSecret); err != nil {
3,726✔
426
                        r.Recorder.Event(order, corev1.EventTypeWarning, "InvalidSecret", fmt.Sprintf("Failed to fetch destination secret '%s': %v", dstEndpoint.Spec.SecretRef.Name, err))
×
427
                        return nil, errLogAndWrap(log, err, "failed to fetch secret for destination")
×
428
                }
×
429
        }
430

431
        // Create a hash based on all related data for idempotency and compute the workflow name
432
        h := sha256.New()
3,904✔
433
        data := []any{
3,904✔
434
                order.Namespace,
3,904✔
435
                artifact.Type, artifact.Spec.Raw, artifactTypeGen,
3,904✔
436
                srcEndpoint.Name,
3,904✔
437
                dstEndpoint.Name,
3,904✔
438
        }
3,904✔
439
        jsonData, err := json.Marshal(data)
3,904✔
440
        if err != nil {
3,904✔
441
                return nil, errLogAndWrap(log, err, "failed to marshal artifact workflow data")
×
442
        }
×
443
        h.Write(jsonData)
3,904✔
444
        sha := hex.EncodeToString(h.Sum(nil))[:16]
3,904✔
445

3,904✔
446
        // We gave all the information to further process this artifact workflow.
3,904✔
447
        // Let's store it to compare it to the current status!
3,904✔
448
        return &desiredAW{
3,904✔
449
                index:       i,
3,904✔
450
                objectMeta:  awObjectMeta(order, sha),
3,904✔
451
                artifact:    artifact,
3,904✔
452
                typeSpec:    artifactTypeSpec,
3,904✔
453
                srcEndpoint: srcEndpoint,
3,904✔
454
                dstEndpoint: dstEndpoint,
3,904✔
455
                srcSecret:   srcSecret,
3,904✔
456
                dstSecret:   dstSecret,
3,904✔
457
                sha:         sha,
3,904✔
458
        }, nil
3,904✔
459
}
460

461
// SetupWithManager sets up the controller with the Manager.
462
func (r *OrderReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
463
        return ctrl.NewControllerManagedBy(mgr).
1✔
464
                For(&arcv1alpha1.Order{}).
1✔
465
                Owns(&arcv1alpha1.ArtifactWorkflow{}).
1✔
466
                Complete(r)
1✔
467
}
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