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

opendefensecloud / artifact-conduit / 19425151842

17 Nov 2025 09:43AM UTC coverage: 42.835% (+1.7%) from 41.116%
19425151842

push

github

web-flow
Workflow PoC (#25)

Closes #9.

114 of 177 new or added lines in 4 files covered. (64.41%)

25 existing lines in 1 file now uncovered.

275 of 642 relevant lines covered (42.83%)

7.2 hits per line

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

79.64
/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 orderFinalizer = "arc.bwi.de/order-finalizer"
31

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

38
//+kubebuilder:rbac:groups=arc.bwi.de,resources=endpoints,verbs=get;list;watch
39
//+kubebuilder:rbac:groups=arc.bwi.de,resources=fragments,verbs=get;list;watch;create;update;patch;delete
40
//+kubebuilder:rbac:groups=arc.bwi.de,resources=orders,verbs=get;list;watch;create;update;patch;delete
41
//+kubebuilder:rbac:groups=arc.bwi.de,resources=orders/status,verbs=get;update;patch
42
//+kubebuilder:rbac:groups=arc.bwi.de,resources=orders/finalizers,verbs=update
43
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
44

45
// Reconcile moves the current state of the cluster closer to the desired state
46
func (r *OrderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
30✔
47
        log := ctrl.LoggerFrom(ctx)
30✔
48

30✔
49
        // Fetch the Order instance
30✔
50
        order := &arcv1alpha1.Order{}
30✔
51
        if err := r.Get(ctx, req.NamespacedName, order); err != nil {
32✔
52
                if apierrors.IsNotFound(err) {
4✔
53
                        // Object not found, return. Created objects are automatically garbage collected.
2✔
54
                        return ctrl.Result{}, nil
2✔
55
                }
2✔
56
                return ctrl.Result{}, err
×
57
        }
58

59
        // Handle deletion: cleanup fragments, then remove finalizer
60
        if !order.DeletionTimestamp.IsZero() {
30✔
61
                log.V(1).Info("Order is being deleted")
2✔
62
                if len(order.Status.Fragments) > 0 {
3✔
63
                        for sha, ref := range order.Status.Fragments {
3✔
64
                                frag := &arcv1alpha1.Fragment{
2✔
65
                                        ObjectMeta: metav1.ObjectMeta{
2✔
66
                                                Namespace: order.Namespace,
2✔
67
                                                Name:      ref.Name,
2✔
68
                                        },
2✔
69
                                }
2✔
70
                                _ = r.Delete(ctx, frag) // ignore not found errors
2✔
71
                                delete(order.Status.Fragments, sha)
2✔
72
                        }
2✔
73
                        if err := r.Status().Update(ctx, order); err != nil {
1✔
74
                                log.Error(err, "Failed to update fragments in Order.Status")
×
75
                                return ctrl.Result{}, err
×
76
                        }
×
77
                        log.V(1).Info("Order fragments cleaned up")
1✔
78
                        // Requeue until all fragments are gone
1✔
79
                        return ctrl.Result{Requeue: true}, nil
1✔
80
                }
81
                // All fragments are gone, remove finalizer
82
                if slices.Contains(order.Finalizers, orderFinalizer) {
2✔
83
                        log.V(1).Info("No fragments, removing finalizer from Order")
1✔
84
                        order.Finalizers = slices.DeleteFunc(order.Finalizers, func(f string) bool {
2✔
85
                                return f == orderFinalizer
1✔
86
                        })
1✔
87
                        if err := r.Update(ctx, order); err != nil {
1✔
88
                                log.Error(err, "Failed to remove finalizer from Order")
×
89
                                return ctrl.Result{}, err
×
90
                        }
×
91
                }
92
                return ctrl.Result{}, nil
1✔
93
        }
94

95
        // Add finalizer if not present and not deleting
96
        if order.DeletionTimestamp.IsZero() {
52✔
97
                if !slices.Contains(order.Finalizers, orderFinalizer) {
32✔
98
                        log.V(1).Info("Adding finalizer to Order")
6✔
99
                        order.Finalizers = append(order.Finalizers, orderFinalizer)
6✔
100
                        if err := r.Update(ctx, order); err != nil {
6✔
101
                                log.Error(err, "Failed to add finalizer to Order")
×
102
                                return ctrl.Result{}, err
×
103
                        }
×
104
                        // Return without requeue; the Update event will trigger reconciliation again
105
                        return ctrl.Result{}, nil
6✔
106
                }
107
        }
108

109
        desiredFrags := map[string]*arcv1alpha1.Fragment{}
20✔
110
        for _, artifact := range order.Spec.Artifacts {
58✔
111
                spec := runtime.RawExtension(artifact.Spec)
38✔
112

38✔
113
                // Let's collect the necessary data for the fragment from the artifact and order
38✔
114
                frag := &arcv1alpha1.Fragment{
38✔
115
                        ObjectMeta: metav1.ObjectMeta{
38✔
116
                                Namespace: order.Namespace,
38✔
117
                                Name:      "",
38✔
118
                        },
38✔
119
                        Spec: arcv1alpha1.FragmentSpec{
38✔
120
                                Type:   artifact.Type,
38✔
121
                                SrcRef: artifact.SrcRef,
38✔
122
                                DstRef: artifact.DstRef,
38✔
123
                                Spec:   spec,
38✔
124
                        },
38✔
125
                }
38✔
126
                if frag.Spec.SrcRef.Name == "" {
45✔
127
                        frag.Spec.SrcRef = order.Spec.Defaults.SrcRef
7✔
128
                }
7✔
129
                if frag.Spec.DstRef.Name == "" {
48✔
130
                        frag.Spec.DstRef = order.Spec.Defaults.DstRef
10✔
131
                }
10✔
132

133
                // Create a hash based on fragment fields for idempotency and compute the fragment name
134
                h := sha256.New()
38✔
135
                data := map[string]any{
38✔
136
                        "type": frag.Spec.Type,
38✔
137
                        "src":  frag.Spec.SrcRef.Name,
38✔
138
                        "dst":  frag.Spec.DstRef.Name,
38✔
139
                        "spec": frag.Spec.Spec.Raw,
38✔
140
                }
38✔
141
                jsonData, err := json.Marshal(data)
38✔
142
                if err != nil {
38✔
143
                        return ctrl.Result{}, fmt.Errorf("failed to marshal fragment data: %w", err)
×
144
                }
×
145
                h.Write(jsonData)
38✔
146
                sha := hex.EncodeToString(h.Sum(nil))[:16]
38✔
147
                frag.Name = fmt.Sprintf("%s-%s", order.Name, sha)
38✔
148

38✔
149
                desiredFrags[sha] = frag
38✔
150
        }
151

152
        // List missing fragments
153
        fragsToCreate := []string{}
20✔
154
        for sha := range desiredFrags {
58✔
155
                _, exists := order.Status.Fragments[sha]
38✔
156
                if exists {
63✔
157
                        continue
25✔
158
                }
159
                fragsToCreate = append(fragsToCreate, sha)
13✔
160
        }
161

162
        // Make sure status is initialized
163
        if order.Status.Fragments == nil {
26✔
164
                order.Status.Fragments = map[string]corev1.LocalObjectReference{}
6✔
165
        }
6✔
166

167
        // Find obsolete fragments
168
        fragsToDelete := []string{}
20✔
169
        for sha := range order.Status.Fragments {
46✔
170
                _, exists := desiredFrags[sha]
26✔
171
                if exists {
51✔
172
                        continue
25✔
173
                }
174
                fragsToDelete = append(fragsToDelete, sha)
1✔
175
        }
176

177
        // Create missing fragments
178
        for _, sha := range fragsToCreate {
33✔
179
                frag := desiredFrags[sha]
13✔
180

13✔
181
                // Set owner reference so Fragment is garbage-collected with the Order
13✔
182
                if err := controllerutil.SetControllerReference(order, frag, r.Scheme); err != nil {
13✔
183
                        return ctrl.Result{}, err
×
184
                }
×
185

186
                if err := r.Create(ctx, frag); err != nil {
13✔
187
                        if apierrors.IsAlreadyExists(err) {
×
188
                                // Already created by a previous reconcile — that's fine
×
189
                                continue
×
190
                        }
191
                        return ctrl.Result{}, err
×
192
                }
193

194
                // Update status
195
                order.Status.Fragments[sha] = corev1.LocalObjectReference{Name: frag.Name}
13✔
196
        }
197

198
        // Delete obsolete fragments
199
        for _, sha := range fragsToDelete {
21✔
200
                // Does not exist anymore, let's clean up!
1✔
201
                if err := r.Delete(ctx, &arcv1alpha1.Fragment{
1✔
202
                        ObjectMeta: metav1.ObjectMeta{
1✔
203
                                Namespace: order.Namespace,
1✔
204
                                Name:      order.Status.Fragments[sha].Name,
1✔
205
                        },
1✔
206
                }); err != nil {
1✔
207
                        return ctrl.Result{}, err
×
208
                }
×
209

210
                // Update status
211
                delete(order.Status.Fragments, sha)
1✔
212
        }
213

214
        // Update status
215
        if len(fragsToCreate) > 0 || len(fragsToDelete) > 0 {
28✔
216
                log.V(1).Info("Updating Order.Status")
8✔
217
                if err := r.Status().Update(ctx, order); err != nil {
8✔
218
                        return ctrl.Result{}, err
×
219
                }
×
220
        }
221

222
        return ctrl.Result{}, nil
20✔
223
}
224

225
// generateReconcileRequestsForSecret generates reconcile requests for all secrets referenced by an Order
226
func (r *OrderReconciler) generateReconcileRequestsForSecret(ctx context.Context, secret client.Object) []reconcile.Request {
39✔
227
        resourcesReferencingSecret := &arcv1alpha1.OrderList{}
39✔
228
        listOps := &client.ListOptions{
39✔
229
                FieldSelector: fields.SelectorFromSet(fields.Set{".spec.srcRef.name": secret.GetName(), ".spec.dstRef.name": secret.GetName()}),
39✔
230
                Namespace:     secret.GetNamespace(),
39✔
231
        }
39✔
232
        err := r.List(ctx, resourcesReferencingSecret, listOps)
39✔
233
        if err != nil {
78✔
234
                return []reconcile.Request{}
39✔
235
        }
39✔
236

NEW
237
        requests := make([]reconcile.Request, len(resourcesReferencingSecret.Items))
×
NEW
238
        for i, item := range resourcesReferencingSecret.Items {
×
NEW
239
                log := ctrl.LoggerFrom(ctx)
×
NEW
240
                log.V(1).Info("Generating reconcile request for resource because referenced secret has changed...")
×
NEW
241
                requests[i] = reconcile.Request{
×
NEW
242
                        NamespacedName: types.NamespacedName{
×
NEW
243
                                Name:      item.GetName(),
×
NEW
244
                                Namespace: item.GetNamespace(),
×
NEW
245
                        },
×
NEW
246
                }
×
NEW
247
        }
×
NEW
248
        return requests
×
249
}
250

251
// SetupWithManager sets up the controller with the Manager.
252
func (r *OrderReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
253
        return ctrl.NewControllerManagedBy(mgr).
1✔
254
                For(&arcv1alpha1.Order{}).
1✔
255
                Watches(
1✔
256
                        &corev1.Secret{},
1✔
257
                        handler.EnqueueRequestsFromMapFunc(r.generateReconcileRequestsForSecret),
1✔
258
                        builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
1✔
259
                ).
1✔
260
                Owns(&arcv1alpha1.Fragment{}).
1✔
261
                Complete(r)
1✔
262
}
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