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

opendefensecloud / artifact-conduit / 21904505262

11 Feb 2026 12:10PM UTC coverage: 83.724% (-0.3%) from 84.058%
21904505262

push

github

web-flow
chore(deps): update dependency python to v3.14.3 (#206)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [python](https://redirect.github.com/actions/python-versions) |
uses-with | minor | `3.13.0` → `3.14.3` |

---

### Release Notes

<details>
<summary>actions/python-versions (python)</summary>

###
[`v3.14.3`](https://redirect.github.com/actions/python-versions/releases/tag/3.14.3-21673711214):
3.14.3

[Compare
Source](https://redirect.github.com/actions/python-versions/compare/3.14.2-20014991423...3.14.3-21673711214)

Python 3.14.3

###
[`v3.14.2`](https://redirect.github.com/actions/python-versions/releases/tag/3.14.2-20014991423):
3.14.2

[Compare
Source](https://redirect.github.com/actions/python-versions/compare/3.14.1-19879739908...3.14.2-20014991423)

Python 3.14.2

###
[`v3.14.1`](https://redirect.github.com/actions/python-versions/releases/tag/3.14.1-19879739908):
3.14.1

[Compare
Source](https://redirect.github.com/actions/python-versions/compare/3.14.0-18313368925...3.14.1-19879739908)

Python 3.14.1

###
[`v3.14.0`](https://redirect.github.com/actions/python-versions/releases/tag/3.14.0-18313368925):
3.14.0

[Compare
Source](https://redirect.github.com/actions/python-versions/compare/3.13.12-21673645133...3.14.0-18313368925)

Python 3.14.0

###
[`v3.13.12`](https://redirect.github.com/actions/python-versions/releases/tag/3.13.12-21673645133):
3.13.12

[Compare
Source](https://redirect.github.com/actions/python-versions/compare/3.13.11-20014977833...3.13.12-21673645133)

Python 3.13.12

###
[`v3.13.11`](https://redirect.github.com/actions/python-versions/releases/tag/3.13.11-20014977833):
3.13.11

[Compare
Source](https://redirect.github.com/actions/python-versions/compare/3.13.10-19879712315...3.13.11-20014977833)

Python 3.13.11

###
[`v3.13.10`](https://redirect.github.com/actions/python-versions/releases/tag/3.13.10-19879712315):
3.13.10

[Compare
Source](https://redirect.github.com/a... (continued)

751 of 897 relevant lines covered (83.72%)

1304.1 hits per line

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

83.43
/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
        corev1 "k8s.io/api/core/v1"
17
        apierrors "k8s.io/apimachinery/pkg/api/errors"
18
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19
        "k8s.io/apimachinery/pkg/runtime"
20
        "k8s.io/client-go/tools/record"
21
        ctrl "sigs.k8s.io/controller-runtime"
22
        "sigs.k8s.io/controller-runtime/pkg/client"
23
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
24

25
        arcv1alpha1 "go.opendefense.cloud/arc/api/arc/v1alpha1"
26
)
27

28
const (
29
        orderFinalizer = "arc.opendefense.cloud/order-finalizer"
30
)
31

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

39
type desiredAW struct {
40
        index       int
41
        objectMeta  metav1.ObjectMeta
42
        artifact    *arcv1alpha1.OrderArtifact
43
        typeSpec    *arcv1alpha1.ArtifactTypeSpec
44
        srcEndpoint *arcv1alpha1.Endpoint
45
        dstEndpoint *arcv1alpha1.Endpoint
46
        srcSecret   *corev1.Secret
47
        dstSecret   *corev1.Secret
48
        sha         string
49
        cron        *arcv1alpha1.Cron
50
}
51

52
//+kubebuilder:rbac:groups=arc.opendefense.cloud,resources=endpoints,verbs=get;list;watch
53
//+kubebuilder:rbac:groups=arc.opendefense.cloud,resources=artifacttypes,verbs=get;list;watch
54
//+kubebuilder:rbac:groups=arc.opendefense.cloud,resources=clusterartifacttypes,verbs=get;list;watch
55
//+kubebuilder:rbac:groups=arc.opendefense.cloud,resources=artifactworkflows,verbs=get;list;watch;create;update;patch;delete
56
//+kubebuilder:rbac:groups=arc.opendefense.cloud,resources=orders,verbs=get;list;watch;create;update;patch;delete
57
//+kubebuilder:rbac:groups=arc.opendefense.cloud,resources=orders/status,verbs=get;update;patch
58
//+kubebuilder:rbac:groups=arc.opendefense.cloud,resources=orders/finalizers,verbs=update
59
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
60
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
61

62
// Reconcile moves the current state of the cluster closer to the desired state
63
func (r *OrderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
2,843✔
64
        log := ctrl.LoggerFrom(ctx)
2,843✔
65
        ctrlResult := ctrl.Result{}
2,843✔
66

2,843✔
67
        // Fetch the Order instance
2,843✔
68
        order := &arcv1alpha1.Order{}
2,843✔
69
        if err := r.Get(ctx, req.NamespacedName, order); err != nil {
2,846✔
70
                if apierrors.IsNotFound(err) {
6✔
71
                        // Object not found, return. Created objects are automatically garbage collected.
3✔
72
                        return ctrlResult, nil
3✔
73
                }
3✔
74

75
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
76
        }
77

78
        // Update last reconcile time
79
        order.Status.LastReconcileAt = metav1.Now()
2,840✔
80

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

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

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

115
                return ctrlResult, nil
1✔
116
        }
117

118
        // Add finalizer if not present and not deleting
119
        if order.DeletionTimestamp.IsZero() {
5,676✔
120
                if !slices.Contains(order.Finalizers, orderFinalizer) {
2,856✔
121
                        log.V(1).Info("Adding finalizer to Order")
18✔
122
                        order.Finalizers = append(order.Finalizers, orderFinalizer)
18✔
123
                        if err := r.Update(ctx, order); err != nil {
18✔
124
                                return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer")
×
125
                        }
×
126
                        // Return without requeue; the Update event will trigger reconciliation again
127
                        return ctrlResult, nil
18✔
128
                }
129
        }
130

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

158
        // Make sure status is initialized
159
        if order.Status.ArtifactWorkflows == nil {
2,913✔
160
                order.Status.ArtifactWorkflows = map[string]arcv1alpha1.OrderArtifactWorkflowStatus{}
95✔
161
        }
95✔
162

163
        // Before we compare to our status, let's fetch all necessary information
164
        // to compute desired state:
165
        desiredAWs := map[string]desiredAW{}
2,818✔
166
        for i, artifact := range order.Spec.Artifacts {
7,676✔
167
                daw, err := r.computeDesiredAW(ctx, log, order, &artifact, i)
4,858✔
168
                if err != nil {
4,980✔
169
                        r.Recorder.Eventf(order, corev1.EventTypeWarning, "ComputationFailed", "Failed to compute desired artifact workflow for artifact index %d: %v", i, err)
122✔
170
                        order.Status.Message = fmt.Sprintf("Failed to compute desired artifact workflow for artifact index %d: %v", i, err)
122✔
171
                        if err := r.Status().Update(ctx, order); err != nil {
122✔
172
                                return ctrlResult, errLogAndWrap(log, err, "failed to update status")
×
173
                        }
×
174

175
                        return ctrlResult, errLogAndWrap(log, err, "failed to compute desired artifact workflow")
122✔
176
                }
177
                desiredAWs[daw.sha] = *daw
4,736✔
178
        }
179
        order.Status.Message = "" // Clear any previous error message
2,696✔
180

2,696✔
181
        // List missing artifact workflows
2,696✔
182
        var createAWs []string
2,696✔
183
        for sha := range desiredAWs {
7,429✔
184
                if _, exists := order.Status.ArtifactWorkflows[sha]; exists {
9,444✔
185
                        continue
4,711✔
186
                }
187

188
                createAWs = append(createAWs, sha)
22✔
189
        }
190

191
        // Find obsolete artifact workflows
192
        var deleteAWs []string
2,696✔
193
        for sha := range order.Status.ArtifactWorkflows {
7,408✔
194
                if _, exists := desiredAWs[sha]; exists {
9,423✔
195
                        continue
4,711✔
196
                }
197

198
                deleteAWs = append(deleteAWs, sha)
1✔
199
        }
200

201
        // Find finished artifact workflows to clean up
202
        var finishedAWs []string
2,696✔
203
        for sha := range order.Status.ArtifactWorkflows {
7,408✔
204
                awStatus := order.Status.ArtifactWorkflows[sha]
4,712✔
205

4,712✔
206
                // Only consider succeeded workflows for TTL cleanup
4,712✔
207
                if awStatus.Phase != arcv1alpha1.WorkflowSucceeded {
9,354✔
208
                        continue
4,642✔
209
                }
210

211
                // Do not clean up ArtifactWorkflows with cron specified
212
                if daw, ok := desiredAWs[sha]; ok && daw.cron != nil {
70✔
213
                        continue
×
214
                }
215

216
                // If TTL is set, check if it has expired
217
                if order.Spec.TTLSecondsAfterCompletion != nil && *order.Spec.TTLSecondsAfterCompletion > 0 {
70✔
218
                        if time.Since(awStatus.CompletionTime.Time) > time.Duration(*order.Spec.TTLSecondsAfterCompletion)*time.Second {
×
219
                                finishedAWs = append(finishedAWs, sha)
×
220
                        } else {
×
221
                                // Requeue when the next TTL expires
×
222
                                ctrlResult.RequeueAfter = time.Duration((*order.Spec.TTLSecondsAfterCompletion)+1)*time.Second - time.Since(awStatus.CompletionTime.Time)
×
223
                        }
×
224

225
                        continue
×
226
                }
227

228
                // No TTL set, cleanup immediately
229
                finishedAWs = append(finishedAWs, sha)
70✔
230
        }
231

232
        // Create missing artifact workflows
233
        for _, sha := range createAWs {
2,718✔
234
                daw := desiredAWs[sha]
22✔
235
                aw, err := r.hydrateArtifactWorkflow(&daw)
22✔
236
                if err != nil {
22✔
237
                        r.Recorder.Eventf(order, corev1.EventTypeWarning, "HydrationFailed", "Failed to hydrate artifact workflow for artifact index %d: %v", daw.index, err)
×
238
                        return ctrlResult, errLogAndWrap(log, err, "failed to hydrate artifact workflow")
×
239
                }
×
240

241
                // Set owner references
242
                if err := controllerutil.SetControllerReference(order, aw, r.Scheme); err != nil {
22✔
243
                        r.Recorder.Eventf(order, corev1.EventTypeWarning, "HydrationFailed", "Failed to set controller reference for artifact workflow: %v", err)
×
244
                        return ctrlResult, errLogAndWrap(log, err, "failed to set controller reference")
×
245
                }
×
246

247
                // Create artifact workflow
248
                if err := r.Create(ctx, aw); err != nil {
23✔
249
                        if apierrors.IsAlreadyExists(err) {
1✔
250
                                // Already created by a previous reconcile — that's fine
×
251
                                continue
×
252
                        }
253
                        r.Recorder.Eventf(order, corev1.EventTypeWarning, "CreationFailed", "Failed to create artifact workflow for artifact index %d: %v", daw.index, err)
1✔
254

1✔
255
                        return ctrlResult, errLogAndWrap(log, err, "failed to create artifact workflow")
1✔
256
                } else {
21✔
257
                        r.Recorder.Eventf(order, corev1.EventTypeNormal, "ArtifactWorkflowCreated", "Created artifact workflow '%s' for artifact index %d", aw.Name, daw.index)
21✔
258
                        log.V(1).Info("Created artifact workflow", "artifactWorkflow", aw.Name)
21✔
259
                }
21✔
260

261
                // Update status
262
                order.Status.ArtifactWorkflows[sha] = arcv1alpha1.OrderArtifactWorkflowStatus{
21✔
263
                        ArtifactIndex: daw.index,
21✔
264
                        WorkflowStatus: arcv1alpha1.WorkflowStatus{
21✔
265
                                Phase: arcv1alpha1.WorkflowUnknown,
21✔
266
                        },
21✔
267
                }
21✔
268
        }
269

270
        // Delete obsolete artifact workflows
271
        for _, sha := range deleteAWs {
2,696✔
272
                // Does not exist anymore, let's clean up!
1✔
273
                if err := r.Delete(ctx, &arcv1alpha1.ArtifactWorkflow{
1✔
274
                        ObjectMeta: awObjectMeta(order, sha),
1✔
275
                }); client.IgnoreNotFound(err) != nil {
1✔
276
                        r.Recorder.Eventf(order, corev1.EventTypeWarning, "DeletionFailed", "Failed to delete obsolete artifact workflow '%s': %v", sha, err)
×
277
                        return ctrlResult, errLogAndWrap(log, err, "failed to delete artifact workflow")
×
278
                }
×
279

280
                // Update status
281
                delete(order.Status.ArtifactWorkflows, sha)
1✔
282
                log.V(1).Info("Deleted obsolete artifact workflow", "artifactWorkflow", sha)
1✔
283
                r.Recorder.Eventf(order, corev1.EventTypeNormal, "ArtifactWorkflowDeleted", "Deleted obsolete artifact workflow '%s'", sha)
1✔
284
        }
285

286
        // Delete finished artifact workflows
287
        for _, sha := range finishedAWs {
2,765✔
288
                // Finished, let's clean up!
70✔
289
                if err := r.Delete(ctx, &arcv1alpha1.ArtifactWorkflow{
70✔
290
                        ObjectMeta: awObjectMeta(order, sha),
70✔
291
                }); client.IgnoreNotFound(err) != nil {
70✔
292
                        r.Recorder.Eventf(order, corev1.EventTypeWarning, "DeletionFailed", "Failed to delete finished artifact workflow '%s': %v", sha, err)
×
293
                        return ctrlResult, errLogAndWrap(log, err, "failed to delete artifact workflow")
×
294
                }
×
295

296
                log.V(1).Info("Deleted finished artifact workflow", "artifactWorkflow", sha)
70✔
297
                r.Recorder.Eventf(order, corev1.EventTypeNormal, "ArtifactWorkflowDeleted", "Deleted finished artifact workflow '%s'", sha)
70✔
298
        }
299

300
        anyPhaseChanged := false
2,695✔
301
        for sha, daw := range desiredAWs {
7,427✔
302
                if slices.Contains(createAWs, sha) {
4,753✔
303
                        // If it was just created we skip the update
21✔
304
                        continue
21✔
305
                }
306
                if daw.cron == nil && order.Status.ArtifactWorkflows[sha].Phase.Completed() {
4,781✔
307
                        // We do not need to check for updates if the workflow is completed and is NOT cron
70✔
308
                        continue
70✔
309
                }
310
                aw := arcv1alpha1.ArtifactWorkflow{}
4,641✔
311
                if err := r.Get(ctx, namespacedName(daw.objectMeta.Namespace, daw.objectMeta.Name), &aw); err != nil {
4,641✔
312
                        delete(order.Status.ArtifactWorkflows, sha)
×
313
                        log.V(1).Info("Artifact workflow not found, deleting from status.", "artifactWorkflow", sha)
×
314
                        if err := r.Status().Update(ctx, order); err != nil {
×
315
                                return ctrlResult, errLogAndWrap(log, err, "failed to update status")
×
316
                        }
×
317

318
                        return ctrlResult, errLogAndWrap(log, err, "failed to get artifact workflow")
×
319
                }
320
                orderAWStatus := order.Status.ArtifactWorkflows[sha]
4,641✔
321
                if orderAWStatus.Phase != aw.Status.Phase ||
4,641✔
322
                        orderAWStatus.Succeeded != aw.Status.Succeeded ||
4,641✔
323
                        orderAWStatus.Failed != aw.Status.Failed ||
4,641✔
324
                        !orderAWStatus.LastScheduled.Equal(aw.Status.LastScheduled) {
6,390✔
325
                        orderAWStatus.WorkflowStatus = aw.Status.WorkflowStatus
1,749✔
326
                        order.Status.ArtifactWorkflows[sha] = orderAWStatus
1,749✔
327
                        anyPhaseChanged = true
1,749✔
328
                }
1,749✔
329
        }
330

331
        // Update status
332
        if len(createAWs) > 0 || len(deleteAWs) > 0 || anyPhaseChanged {
4,365✔
333
                log.V(1).Info("Updating order status")
1,670✔
334
                // Make sure ArtifactIndex is up to date
1,670✔
335
                for sha, daw := range desiredAWs {
4,634✔
336
                        aws := order.Status.ArtifactWorkflows[sha]
2,964✔
337
                        aws.ArtifactIndex = daw.index
2,964✔
338
                        order.Status.ArtifactWorkflows[sha] = aws
2,964✔
339
                }
2,964✔
340
                if err := r.Status().Update(ctx, order); err != nil {
1,758✔
341
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
88✔
342
                }
88✔
343
        }
344

345
        return ctrlResult, nil
2,607✔
346
}
347

348
func (r *OrderReconciler) hydrateArtifactWorkflow(daw *desiredAW) (*arcv1alpha1.ArtifactWorkflow, error) {
22✔
349
        params, err := dawToParameters(daw)
22✔
350
        if err != nil {
22✔
351
                return nil, err
×
352
        }
×
353

354
        // Next we create the ArtifactWorkflow instance
355
        aw := &arcv1alpha1.ArtifactWorkflow{
22✔
356
                ObjectMeta: daw.objectMeta,
22✔
357
                Spec: arcv1alpha1.ArtifactWorkflowSpec{
22✔
358
                        WorkflowTemplateRef: daw.typeSpec.WorkflowTemplateRef,
22✔
359
                        Parameters:          params,
22✔
360
                        SrcSecretRef:        daw.srcEndpoint.Spec.SecretRef,
22✔
361
                        DstSecretRef:        daw.dstEndpoint.Spec.SecretRef,
22✔
362
                        Cron:                daw.cron,
22✔
363
                },
22✔
364
        }
22✔
365

22✔
366
        return aw, nil
22✔
367
}
368

369
func (r *OrderReconciler) computeDesiredAW(ctx context.Context, log logr.Logger, order *arcv1alpha1.Order, artifact *arcv1alpha1.OrderArtifact, i int) (*desiredAW, error) {
4,858✔
370
        log = log.WithValues("artifactIndex", i)
4,858✔
371

4,858✔
372
        // We need the referenced src- and dst-endpoints for the artifact
4,858✔
373
        srcRefName := artifact.SrcRef.Name
4,858✔
374
        if srcRefName == "" {
5,289✔
375
                srcRefName = order.Spec.Defaults.SrcRef.Name
431✔
376
        }
431✔
377
        dstRefName := artifact.DstRef.Name
4,858✔
378
        if dstRefName == "" {
5,406✔
379
                dstRefName = order.Spec.Defaults.DstRef.Name
548✔
380
        }
548✔
381

382
        srcEndpoint := &arcv1alpha1.Endpoint{}
4,858✔
383
        if err := r.Get(ctx, namespacedName(order.Namespace, srcRefName), srcEndpoint); err != nil {
4,872✔
384
                r.Recorder.Eventf(order, corev1.EventTypeWarning, "InvalidEndpoint", "Failed to fetch source endpoint '%s': %v", srcRefName, err)
14✔
385
                return nil, errLogAndWrap(log, err, "failed to fetch endpoint for source")
14✔
386
        }
14✔
387
        dstEndpoint := &arcv1alpha1.Endpoint{}
4,844✔
388
        if err := r.Get(ctx, namespacedName(order.Namespace, dstRefName), dstEndpoint); err != nil {
4,856✔
389
                r.Recorder.Eventf(order, corev1.EventTypeWarning, "InvalidEndpoint", "Failed to fetch destination endpoint '%s': %v", dstRefName, err)
12✔
390
                return nil, errLogAndWrap(log, err, "failed to fetch endpoint for destination")
12✔
391
        }
12✔
392

393
        // Validate that the endpoint usage is correct
394
        if srcEndpoint.Spec.Usage != arcv1alpha1.EndpointUsagePullOnly && srcEndpoint.Spec.Usage != arcv1alpha1.EndpointUsageAll {
4,846✔
395
                err := fmt.Errorf("endpoint '%s' usage '%s' is not compatible with source usage", srcEndpoint.Name, srcEndpoint.Spec.Usage)
14✔
396
                r.Recorder.Eventf(order, corev1.EventTypeWarning, "InvalidEndpoint", "Source endpoint '%s' has incompatible usage '%s'", srcEndpoint.Name, srcEndpoint.Spec.Usage)
14✔
397

14✔
398
                return nil, errLogAndWrap(log, err, "artifact validation failed")
14✔
399
        }
14✔
400
        if dstEndpoint.Spec.Usage != arcv1alpha1.EndpointUsagePushOnly && dstEndpoint.Spec.Usage != arcv1alpha1.EndpointUsageAll {
4,832✔
401
                err := fmt.Errorf("endpoint '%s' usage '%s' is not compatible with destination usage", dstEndpoint.Name, dstEndpoint.Spec.Usage)
14✔
402
                r.Recorder.Eventf(order, corev1.EventTypeWarning, "InvalidEndpoint", "Destination endpoint '%s' has incompatible usage '%s'", dstEndpoint.Name, dstEndpoint.Spec.Usage)
14✔
403

14✔
404
                return nil, errLogAndWrap(log, err, "artifact validation failed")
14✔
405
        }
14✔
406

407
        // Validate against ArtifactType rules
408
        artifactType := &arcv1alpha1.ArtifactType{}
4,804✔
409
        if err := r.Get(ctx, namespacedName(order.Namespace, artifact.Type), artifactType); client.IgnoreNotFound(err) != nil {
4,804✔
410
                r.Recorder.Eventf(order, corev1.EventTypeWarning, "InvalidArtifactType", "Failed to fetch ArtifactType '%s': %v", artifact.Type, err)
×
411
                return nil, errLogAndWrap(log, err, "failed to fetch referenced ArtifactType")
×
412
        }
×
413
        var (
4,804✔
414
                artifactTypeGen  int64
4,804✔
415
                artifactTypeSpec *arcv1alpha1.ArtifactTypeSpec
4,804✔
416
        )
4,804✔
417
        if artifactType.Name == "" { // was not found, let's check ClusterArtifactType
9,487✔
418
                clusterArtifactType := &arcv1alpha1.ClusterArtifactType{}
4,683✔
419
                if err := r.Get(ctx, namespacedName("", artifact.Type), clusterArtifactType); err != nil {
4,740✔
420
                        return nil, errLogAndWrap(log, err, "failed to fetch ArtifactType or ClusterArtifactType")
57✔
421
                }
57✔
422
                artifactTypeSpec = &clusterArtifactType.Spec
4,626✔
423
                artifactTypeGen = clusterArtifactType.Generation
4,626✔
424
                // NOTE: ClusterArtifactTypes can only reference ClusterWorkflowTemplates, so we enforce this here:
4,626✔
425
                artifactTypeSpec.WorkflowTemplateRef.ClusterScope = true
4,626✔
426
        } else {
121✔
427
                artifactTypeSpec = &artifactType.Spec
121✔
428
                artifactTypeGen = artifactType.Generation
121✔
429
        }
121✔
430

431
        if len(artifactTypeSpec.Rules.SrcTypes) > 0 && !slices.Contains(artifactTypeSpec.Rules.SrcTypes, srcEndpoint.Spec.Type) {
4,758✔
432
                err := fmt.Errorf("source endpoint type '%s' is not allowed by ArtifactType rules", srcEndpoint.Spec.Type)
11✔
433
                r.Recorder.Eventf(order, corev1.EventTypeWarning, "InvalidArtifactType", "Source endpoint type '%s' is not allowed by ArtifactType '%s' rules", srcEndpoint.Spec.Type, artifact.Type)
11✔
434

11✔
435
                return nil, errLogAndWrap(log, err, "artifact validation failed")
11✔
436
        }
11✔
437
        if len(artifactTypeSpec.Rules.DstTypes) > 0 && !slices.Contains(artifactTypeSpec.Rules.DstTypes, dstEndpoint.Spec.Type) {
4,736✔
438
                err := fmt.Errorf("destination endpoint type '%s' is not allowed by ArtifactType rules", dstEndpoint.Spec.Type)
×
439
                r.Recorder.Eventf(order, corev1.EventTypeWarning, "InvalidArtifactType", "Destination endpoint type '%s' is not allowed by ArtifactType '%s' rules", dstEndpoint.Spec.Type, artifact.Type)
×
440

×
441
                return nil, errLogAndWrap(log, err, "artifact validation failed")
×
442
        }
×
443

444
        // Next, we need the secret contents
445
        srcSecret := &corev1.Secret{}
4,736✔
446
        if srcEndpoint.Spec.SecretRef.Name != "" {
9,469✔
447
                if err := r.Get(ctx, namespacedName(order.Namespace, srcEndpoint.Spec.SecretRef.Name), srcSecret); err != nil {
4,733✔
448
                        r.Recorder.Eventf(order, corev1.EventTypeWarning, "InvalidSecret", "Failed to fetch source secret '%s': %v", srcEndpoint.Spec.SecretRef.Name, err)
×
449
                        return nil, errLogAndWrap(log, err, "failed to fetch secret for source")
×
450
                }
×
451
        }
452

453
        dstSecret := &corev1.Secret{}
4,736✔
454
        if dstEndpoint.Spec.SecretRef.Name != "" {
9,469✔
455
                if err := r.Get(ctx, namespacedName(order.Namespace, dstEndpoint.Spec.SecretRef.Name), dstSecret); err != nil {
4,733✔
456
                        r.Recorder.Eventf(order, corev1.EventTypeWarning, "InvalidSecret", "Failed to fetch destination secret '%s': %v", dstEndpoint.Spec.SecretRef.Name, err)
×
457
                        return nil, errLogAndWrap(log, err, "failed to fetch secret for destination")
×
458
                }
×
459
        }
460

461
        // Cron schedule if any
462
        cron := artifact.Cron
4,736✔
463
        if cron == nil {
9,468✔
464
                cron = order.Spec.Defaults.Cron
4,732✔
465
        }
4,732✔
466

467
        // Create a hash based on all related data for idempotency and compute the workflow name
468
        h := sha256.New()
4,736✔
469
        data := []any{
4,736✔
470
                order.Namespace,
4,736✔
471
                artifact.Type,
4,736✔
472
                artifact.Spec.Raw,
4,736✔
473
                artifactTypeGen,
4,736✔
474
                srcEndpoint.Name,
4,736✔
475
                dstEndpoint.Name,
4,736✔
476
                order.Status.LastForceAt,
4,736✔
477
                cron,
4,736✔
478
        }
4,736✔
479

4,736✔
480
        if err := json.NewEncoder(h).Encode(data); err != nil {
4,736✔
481
                return nil, errLogAndWrap(log, err, "failed to marshal artifact workflow data")
×
482
        }
×
483

484
        sha := hex.EncodeToString(h.Sum(nil))[:16]
4,736✔
485

4,736✔
486
        // We gave all the information to further process this artifact workflow.
4,736✔
487
        // Let's store it to compare it to the current status!
4,736✔
488
        return &desiredAW{
4,736✔
489
                index:       i,
4,736✔
490
                objectMeta:  awObjectMeta(order, sha),
4,736✔
491
                artifact:    artifact,
4,736✔
492
                typeSpec:    artifactTypeSpec,
4,736✔
493
                srcEndpoint: srcEndpoint,
4,736✔
494
                dstEndpoint: dstEndpoint,
4,736✔
495
                srcSecret:   srcSecret,
4,736✔
496
                dstSecret:   dstSecret,
4,736✔
497
                sha:         sha,
4,736✔
498
                cron:        cron,
4,736✔
499
        }, nil
4,736✔
500
}
501

502
// SetupWithManager sets up the controller with the Manager.
503
func (r *OrderReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
504
        return ctrl.NewControllerManagedBy(mgr).
1✔
505
                For(&arcv1alpha1.Order{}).
1✔
506
                Owns(&arcv1alpha1.ArtifactWorkflow{}).
1✔
507
                Complete(r)
1✔
508
}
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