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

k8snetworkplumbingwg / dra-driver-sriov / 23236005426

18 Mar 2026 08:36AM UTC coverage: 43.91% (-1.2%) from 45.1%
23236005426

Pull #69

github

web-flow
Merge 235a4f1e1 into 67fe1e4f9
Pull Request #69: Decouple device attributes from policy with new DeviceAttributes CRD

235 of 413 new or added lines in 7 files covered. (56.9%)

4 existing lines in 3 files now uncovered.

1388 of 3161 relevant lines covered (43.91%)

3.08 hits per line

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

76.23
/pkg/controller/resourcepolicycontroller.go
1
/*
2
 * Copyright 2025 The Kubernetes Authors.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package controller
18

19
import (
20
        "context"
21
        "sort"
22
        "time"
23

24
        corev1 "k8s.io/api/core/v1"
25
        resourceapi "k8s.io/api/resource/v1"
26
        apierrors "k8s.io/apimachinery/pkg/api/errors"
27
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28
        "k8s.io/apimachinery/pkg/labels"
29
        "k8s.io/apimachinery/pkg/types"
30
        "k8s.io/client-go/util/workqueue"
31
        "k8s.io/klog/v2"
32
        ctrl "sigs.k8s.io/controller-runtime"
33
        "sigs.k8s.io/controller-runtime/pkg/client"
34
        "sigs.k8s.io/controller-runtime/pkg/event"
35
        "sigs.k8s.io/controller-runtime/pkg/handler"
36
        "sigs.k8s.io/controller-runtime/pkg/predicate"
37
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
38
        "sigs.k8s.io/controller-runtime/pkg/source"
39

40
        sriovdrav1alpha1 "github.com/k8snetworkplumbingwg/dra-driver-sriov/pkg/api/sriovdra/v1alpha1"
41
        "github.com/k8snetworkplumbingwg/dra-driver-sriov/pkg/consts"
42
        "github.com/k8snetworkplumbingwg/dra-driver-sriov/pkg/devicestate"
43
)
44

45
const (
46
        resourcePolicySyncEventName = "resource-policy-sync"
47
)
48

49
// SriovResourcePolicyReconciler reconciles SriovResourcePolicy and DeviceAttributes objects
50
type SriovResourcePolicyReconciler struct {
51
        client.Client
52
        nodeName           string
53
        namespace          string
54
        log                klog.Logger
55
        deviceStateManager devicestate.DeviceState
56
}
57

58
// NewSriovResourcePolicyReconciler creates a new SriovResourcePolicyReconciler
59
func NewSriovResourcePolicyReconciler(client client.Client, nodeName, namespace string, deviceStateManager devicestate.DeviceState) *SriovResourcePolicyReconciler {
2✔
60
        return &SriovResourcePolicyReconciler{
2✔
61
                Client:             client,
2✔
62
                deviceStateManager: deviceStateManager,
2✔
63
                nodeName:           nodeName,
2✔
64
                namespace:          namespace,
2✔
65
                log:                klog.Background().WithName("SriovResourcePolicy"),
2✔
66
        }
2✔
67
}
2✔
68

69
// Reconcile handles reconciliation of SriovResourcePolicy and DeviceAttributes resources.
70
// It builds the full picture of which devices to advertise and which attributes to apply.
71
func (r *SriovResourcePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
8✔
72
        r.log.Info("Starting reconcile", "request", req.NamespacedName, "watchedNamespace", r.namespace)
8✔
73

8✔
74
        node := &metav1.PartialObjectMetadata{}
8✔
75
        node.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Node"))
8✔
76
        if err := r.Get(ctx, types.NamespacedName{Name: r.nodeName}, node); err != nil {
9✔
77
                if apierrors.IsNotFound(err) {
2✔
78
                        r.log.Error(err, "Node not found", "nodeName", r.nodeName)
1✔
79
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
1✔
80
                }
1✔
NEW
81
                r.log.Error(err, "Failed to get node", "nodeName", r.nodeName)
×
NEW
82
                return ctrl.Result{}, err
×
83
        }
84

85
        // List all SriovResourcePolicy objects in the operator namespace
86
        resourcePolicyList := &sriovdrav1alpha1.SriovResourcePolicyList{}
7✔
87
        if err := r.List(ctx, resourcePolicyList, client.InNamespace(r.namespace)); err != nil {
7✔
NEW
88
                r.log.Error(err, "Failed to list SriovResourcePolicy objects", "namespace", r.namespace)
×
NEW
89
                return ctrl.Result{}, err
×
NEW
90
        }
×
91

92
        // List all DeviceAttributes objects in the operator namespace
93
        deviceAttrList := &sriovdrav1alpha1.DeviceAttributesList{}
7✔
94
        if err := r.List(ctx, deviceAttrList, client.InNamespace(r.namespace)); err != nil {
7✔
NEW
95
                r.log.Error(err, "Failed to list DeviceAttributes objects", "namespace", r.namespace)
×
NEW
96
                return ctrl.Result{}, err
×
NEW
97
        }
×
98

99
        // Find matching resource policies for this node
100
        var matchingPolicies []*sriovdrav1alpha1.SriovResourcePolicy
7✔
101
        for i := range resourcePolicyList.Items {
13✔
102
                policy := &resourcePolicyList.Items[i]
6✔
103
                if r.matchesNodeSelector(node.Labels, policy.Spec.NodeSelector) {
12✔
104
                        matchingPolicies = append(matchingPolicies, policy)
6✔
105
                }
6✔
106
        }
107

108
        policyDevices := r.getPolicyDeviceMap(matchingPolicies, deviceAttrList.Items)
7✔
109
        if err := r.deviceStateManager.UpdatePolicyDevices(ctx, policyDevices); err != nil {
7✔
NEW
110
                r.log.Error(err, "Failed to update policy devices")
×
NEW
111
                return ctrl.Result{}, err
×
NEW
112
        }
×
113

114
        return ctrl.Result{}, nil
7✔
115
}
116

117
// getPolicyDeviceMap builds the full map of device name -> attributes for all
118
// devices matched by the given policies. DeviceAttributes are resolved via
119
// each config's DeviceAttributesSelector.
120
func (r *SriovResourcePolicyReconciler) getPolicyDeviceMap(
121
        policies []*sriovdrav1alpha1.SriovResourcePolicy,
122
        allDeviceAttrs []sriovdrav1alpha1.DeviceAttributes,
123
) map[string]map[resourceapi.QualifiedName]resourceapi.DeviceAttribute {
10✔
124
        policyDevices := make(map[string]map[resourceapi.QualifiedName]resourceapi.DeviceAttribute)
10✔
125

10✔
126
        if len(policies) == 0 {
13✔
127
                r.log.Info("No matching SriovResourcePolicy found for node", "nodeName", r.nodeName)
3✔
128
                return policyDevices
3✔
129
        }
3✔
130

131
        allocatableDevices := r.deviceStateManager.GetAllocatableDevices()
7✔
132

7✔
133
        sort.Slice(policies, func(i, j int) bool {
8✔
134
                return policies[i].Name < policies[j].Name
1✔
135
        })
1✔
136
        for _, policy := range policies {
15✔
137
                r.log.V(2).Info("Processing policy",
8✔
138
                        "policyName", policy.Name,
8✔
139
                        "configCount", len(policy.Spec.Configs),
8✔
140
                        "totalDevices", len(allocatableDevices))
8✔
141

8✔
142
                for _, config := range policy.Spec.Configs {
16✔
143
                        resolvedAttrs := r.resolveDeviceAttributes(config.DeviceAttributesSelector, allDeviceAttrs)
8✔
144

8✔
145
                        for deviceName, device := range allocatableDevices {
23✔
146
                                if _, exists := policyDevices[deviceName]; exists {
17✔
147
                                        continue
2✔
148
                                }
149

150
                                if r.deviceMatchesFilters(device, config.ResourceFilters) {
26✔
151
                                        attrs := make(map[resourceapi.QualifiedName]resourceapi.DeviceAttribute, len(resolvedAttrs))
13✔
152
                                        for k, v := range resolvedAttrs {
16✔
153
                                                attrs[k] = v
3✔
154
                                        }
3✔
155
                                        policyDevices[deviceName] = attrs
13✔
156
                                        r.log.V(2).Info("Device matches config filter",
13✔
157
                                                "deviceName", deviceName,
13✔
158
                                                "policyName", policy.Name,
13✔
159
                                                "device", device,
13✔
160
                                                "attributes", attrs)
13✔
161
                                }
162
                        }
163
                }
164
        }
165

166
        r.log.Info("Policy devices resolved",
7✔
167
                "matchingDevices", len(policyDevices),
7✔
168
                "totalDevices", len(allocatableDevices))
7✔
169
        r.log.V(2).Info("Policy devices details", "policyDevices", policyDevices)
7✔
170

7✔
171
        return policyDevices
7✔
172
}
173

174
// resolveDeviceAttributes finds all DeviceAttributes objects matching the
175
// given label selector and merges their attributes. When multiple objects
176
// match and define the same key, the value from the alphabetically last
177
// object name wins (deterministic).
178
func (r *SriovResourcePolicyReconciler) resolveDeviceAttributes(
179
        selector *metav1.LabelSelector,
180
        allDeviceAttrs []sriovdrav1alpha1.DeviceAttributes,
181
) map[resourceapi.QualifiedName]resourceapi.DeviceAttribute {
8✔
182
        if selector == nil {
14✔
183
                return nil
6✔
184
        }
6✔
185

186
        sel, err := metav1.LabelSelectorAsSelector(selector)
2✔
187
        if err != nil {
2✔
NEW
188
                r.log.Error(err, "Invalid DeviceAttributesSelector")
×
NEW
189
                return nil
×
NEW
190
        }
×
191

192
        // Collect matching DeviceAttributes, sort by name for determinism
193
        var matched []sriovdrav1alpha1.DeviceAttributes
2✔
194
        for i := range allDeviceAttrs {
4✔
195
                da := allDeviceAttrs[i]
2✔
196
                if sel.Matches(labels.Set(da.Labels)) {
4✔
197
                        matched = append(matched, da)
2✔
198
                }
2✔
199
        }
200
        sort.Slice(matched, func(i, j int) bool {
2✔
NEW
201
                return matched[i].Name < matched[j].Name
×
NEW
202
        })
×
203

204
        merged := make(map[resourceapi.QualifiedName]resourceapi.DeviceAttribute)
2✔
205
        for _, da := range matched {
4✔
206
                for key, val := range da.Spec.Attributes {
4✔
207
                        merged[key] = val
2✔
208
                }
2✔
209
        }
210

211
        return merged
2✔
212
}
213

214
// matchesNodeSelector checks if node labels match the given selector
215
func (r *SriovResourcePolicyReconciler) matchesNodeSelector(nodeLabels map[string]string, nodeSelector map[string]string) bool {
9✔
216
        if len(nodeSelector) == 0 {
16✔
217
                return true
7✔
218
        }
7✔
219

220
        selector := labels.Set(nodeSelector).AsSelector()
2✔
221
        return selector.Matches(labels.Set(nodeLabels))
2✔
222
}
223

224
// deviceMatchesFilters checks if a device matches any of the provided resource filters.
225
// Empty filters list matches all devices.
226
func (r *SriovResourcePolicyReconciler) deviceMatchesFilters(device resourceapi.Device, filters []sriovdrav1alpha1.ResourceFilter) bool {
13✔
227
        if len(filters) == 0 {
23✔
228
                return true
10✔
229
        }
10✔
230

231
        for _, filter := range filters {
6✔
232
                if r.deviceMatchesFilter(device, filter) {
6✔
233
                        return true
3✔
234
                }
3✔
235
        }
236

NEW
237
        return false
×
238
}
239

240
// deviceMatchesFilter checks if a device matches a specific resource filter
241
func (r *SriovResourcePolicyReconciler) deviceMatchesFilter(device resourceapi.Device, filter sriovdrav1alpha1.ResourceFilter) bool {
10✔
242
        if len(filter.Vendors) > 0 {
15✔
243
                vendorAttr, exists := device.Attributes[consts.AttributeVendorID]
5✔
244
                if !exists || vendorAttr.StringValue == nil {
5✔
NEW
245
                        return false
×
NEW
246
                }
×
247
                if !stringSliceContains(filter.Vendors, *vendorAttr.StringValue) {
6✔
248
                        return false
1✔
249
                }
1✔
250
        }
251

252
        if len(filter.Devices) > 0 {
11✔
253
                deviceAttr, exists := device.Attributes[consts.AttributeDeviceID]
2✔
254
                if !exists || deviceAttr.StringValue == nil {
2✔
NEW
255
                        return false
×
NEW
256
                }
×
257
                if !stringSliceContains(filter.Devices, *deviceAttr.StringValue) {
3✔
258
                        return false
1✔
259
                }
1✔
260
        }
261

262
        if len(filter.PciAddresses) > 0 {
10✔
263
                pciAttr, exists := device.Attributes[consts.AttributePciAddress]
2✔
264
                if !exists || pciAttr.StringValue == nil {
2✔
NEW
265
                        return false
×
NEW
266
                }
×
267
                if !stringSliceContains(filter.PciAddresses, *pciAttr.StringValue) {
3✔
268
                        return false
1✔
269
                }
1✔
270
        }
271

272
        if len(filter.PfNames) > 0 {
9✔
273
                pfAttr, exists := device.Attributes[consts.AttributePFName]
2✔
274
                if !exists || pfAttr.StringValue == nil {
2✔
NEW
275
                        return false
×
NEW
276
                }
×
277
                if !stringSliceContains(filter.PfNames, *pfAttr.StringValue) {
3✔
278
                        return false
1✔
279
                }
1✔
280
        }
281

282
        if len(filter.PfPciAddresses) > 0 {
8✔
283
                parentAttr, exists := device.Attributes[consts.AttributePfPciAddress]
2✔
284
                if !exists || parentAttr.StringValue == nil {
2✔
NEW
285
                        return false
×
NEW
286
                }
×
287
                if !stringSliceContains(filter.PfPciAddresses, *parentAttr.StringValue) {
3✔
288
                        return false
1✔
289
                }
1✔
290
        }
291

292
        // TODO: Implement driver checking if needed
293
        if len(filter.Drivers) > 0 {
5✔
NEW
294
                r.log.V(3).Info("Driver filtering not yet implemented", "deviceName", device.Name)
×
NEW
295
        }
×
296

297
        return true
5✔
298
}
299

300
func stringSliceContains(slice []string, item string) bool {
15✔
301
        for _, s := range slice {
32✔
302
                if s == item {
26✔
303
                        return true
9✔
304
                }
9✔
305
        }
306
        return false
6✔
307
}
308

309
// SetupWithManager sets up the controller with the Manager.
310
func (r *SriovResourcePolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
311
        qHandler := func(q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
7✔
312
                q.AddAfter(reconcile.Request{NamespacedName: types.NamespacedName{
6✔
313
                        Namespace: r.namespace,
6✔
314
                        Name:      resourcePolicySyncEventName,
6✔
315
                }}, time.Second)
6✔
316
        }
6✔
317

318
        delayedEventHandler := handler.Funcs{
1✔
319
                CreateFunc: func(_ context.Context, e event.TypedCreateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
5✔
320
                        r.log.Info("Enqueuing sync for create event",
4✔
321
                                "resource", e.Object.GetName(),
4✔
322
                                "type", e.Object.GetObjectKind().GroupVersionKind().String())
4✔
323
                        qHandler(w)
4✔
324
                },
4✔
NEW
325
                UpdateFunc: func(_ context.Context, e event.TypedUpdateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
×
NEW
326
                        r.log.Info("Enqueuing sync for update event",
×
NEW
327
                                "resource", e.ObjectNew.GetName(),
×
NEW
328
                                "type", e.ObjectNew.GetObjectKind().GroupVersionKind().String())
×
NEW
329
                        qHandler(w)
×
NEW
330
                },
×
331
                DeleteFunc: func(_ context.Context, e event.TypedDeleteEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
2✔
332
                        r.log.Info("Enqueuing sync for delete event",
2✔
333
                                "resource", e.Object.GetName(),
2✔
334
                                "type", e.Object.GetObjectKind().GroupVersionKind().String())
2✔
335
                        qHandler(w)
2✔
336
                },
2✔
NEW
337
                GenericFunc: func(_ context.Context, e event.TypedGenericEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
×
NEW
338
                        r.log.Info("Enqueuing sync for generic event",
×
NEW
339
                                "resource", e.Object.GetName(),
×
NEW
340
                                "type", e.Object.GetObjectKind().GroupVersionKind().String())
×
NEW
341
                        qHandler(w)
×
NEW
342
                },
×
343
        }
344

345
        nodeEventHandler := handler.Funcs{
1✔
346
                CreateFunc: func(_ context.Context, e event.TypedCreateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
1✔
NEW
347
                        if e.Object.GetName() == r.nodeName {
×
NEW
348
                                r.log.Info("Enqueuing sync for node create event", "node", e.Object.GetName())
×
NEW
349
                                qHandler(w)
×
NEW
350
                        }
×
351
                },
NEW
352
                UpdateFunc: func(_ context.Context, e event.TypedUpdateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
×
NEW
353
                        if e.ObjectNew.GetName() == r.nodeName {
×
NEW
354
                                oldLabels := e.ObjectOld.GetLabels()
×
NEW
355
                                newLabels := e.ObjectNew.GetLabels()
×
NEW
356
                                if !labels.Equals(oldLabels, newLabels) {
×
NEW
357
                                        r.log.Info("Enqueuing sync for node label change event", "node", e.ObjectNew.GetName())
×
NEW
358
                                        qHandler(w)
×
NEW
359
                                }
×
360
                        }
361
                },
NEW
362
                DeleteFunc: func(_ context.Context, e event.TypedDeleteEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
×
NEW
363
                        if e.Object.GetName() == r.nodeName {
×
NEW
364
                                r.log.Info("Enqueuing sync for node delete event", "node", e.Object.GetName())
×
NEW
365
                                qHandler(w)
×
NEW
366
                        }
×
367
                },
368
        }
369

370
        var eventChan = make(chan event.GenericEvent, 1)
1✔
371
        eventChan <- event.GenericEvent{Object: &sriovdrav1alpha1.SriovResourcePolicy{
1✔
372
                ObjectMeta: metav1.ObjectMeta{Name: resourcePolicySyncEventName, Namespace: r.namespace}}}
1✔
373
        close(eventChan)
1✔
374

1✔
375
        namespacePredicate := predicate.NewPredicateFuncs(func(obj client.Object) bool {
15✔
376
                return obj.GetNamespace() == r.namespace
14✔
377
        })
14✔
378

379
        nodeMetadata := &metav1.PartialObjectMetadata{}
1✔
380
        nodeMetadata.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Node"))
1✔
381

1✔
382
        return ctrl.NewControllerManagedBy(mgr).
1✔
383
                For(&sriovdrav1alpha1.SriovResourcePolicy{}).
1✔
384
                Watches(nodeMetadata, nodeEventHandler).
1✔
385
                Watches(&sriovdrav1alpha1.SriovResourcePolicy{}, delayedEventHandler).
1✔
386
                Watches(&sriovdrav1alpha1.DeviceAttributes{}, delayedEventHandler).
1✔
387
                WithEventFilter(namespacePredicate).
1✔
388
                WatchesRawSource(source.Channel(eventChan, &handler.EnqueueRequestForObject{})).
1✔
389
                Complete(r)
1✔
390
}
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