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

opendefensecloud / artifact-conduit / 19361260392

14 Nov 2025 10:10AM UTC coverage: 36.051% (-6.1%) from 42.149%
19361260392

Pull #25

github

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

14 of 87 new or added lines in 4 files covered. (16.09%)

6 existing lines in 2 files now uncovered.

199 of 552 relevant lines covered (36.05%)

3.67 hits per line

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

73.01
/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/client"
23
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
24
        "sigs.k8s.io/controller-runtime/pkg/handler"
25
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
26
)
27

28
const orderFinalizer = "arc.bwi.de/order-finalizer"
29

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

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

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

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

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

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

106
        desiredFrags := map[string]*arcv1alpha1.Fragment{}
17✔
107
        for _, artifact := range order.Spec.Artifacts {
48✔
108
                spec := runtime.RawExtension(artifact.Spec)
31✔
109

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

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

31✔
146
                desiredFrags[sha] = frag
31✔
147
        }
148

149
        // List missing fragments
150
        fragsToCreate := []string{}
17✔
151
        for sha := range desiredFrags {
48✔
152
                _, exists := order.Status.Fragments[sha]
31✔
153
                if exists {
49✔
154
                        continue
18✔
155
                }
156
                fragsToCreate = append(fragsToCreate, sha)
13✔
157
        }
158

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

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

174
        // Create missing fragments
175
        for _, sha := range fragsToCreate {
30✔
176
                frag := desiredFrags[sha]
13✔
177

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

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

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

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

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

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

219
        return ctrl.Result{}, nil
17✔
220
}
221

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

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

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