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

opendefensecloud / artifact-conduit / 19362054263

14 Nov 2025 10:41AM UTC coverage: 35.366% (-6.8%) from 42.149%
19362054263

Pull #25

github

web-flow
Merge 0a1054bc3 into 3e03bbe6b
Pull Request #25: Workflow PoC

18 of 109 new or added lines in 4 files covered. (16.51%)

6 existing lines in 2 files now uncovered.

203 of 574 relevant lines covered (35.37%)

3.43 hits per line

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

73.65
/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=fragments,verbs=get;list;watch;create;update;patch;delete
39
//+kubebuilder:rbac:groups=arc.bwi.de,resources=orders,verbs=get;list;watch;create;update;patch;delete
40
//+kubebuilder:rbac:groups=arc.bwi.de,resources=orders/status,verbs=get;update;patch
41
//+kubebuilder:rbac:groups=arc.bwi.de,resources=orders/finalizers,verbs=update
42
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
43

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

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

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

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

108
        desiredFrags := map[string]*arcv1alpha1.Fragment{}
16✔
109
        for _, artifact := range order.Spec.Artifacts {
46✔
110
                spec := runtime.RawExtension(artifact.Spec)
30✔
111

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

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

30✔
148
                desiredFrags[sha] = frag
30✔
149
        }
150

151
        // List missing fragments
152
        fragsToCreate := []string{}
16✔
153
        for sha := range desiredFrags {
46✔
154
                _, exists := order.Status.Fragments[sha]
30✔
155
                if exists {
47✔
156
                        continue
17✔
157
                }
158
                fragsToCreate = append(fragsToCreate, sha)
13✔
159
        }
160

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

166
        // Find obsolete fragments
167
        fragsToDelete := []string{}
16✔
168
        for sha := range order.Status.Fragments {
34✔
169
                _, exists := desiredFrags[sha]
18✔
170
                if exists {
35✔
171
                        continue
17✔
172
                }
173
                fragsToDelete = append(fragsToDelete, sha)
1✔
174
        }
175

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

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

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

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

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

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

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

221
        return ctrl.Result{}, nil
16✔
222
}
223

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

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

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