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

k8snetworkplumbingwg / dra-driver-sriov / 19530478236

20 Nov 2025 08:29AM UTC coverage: 36.74% (+1.5%) from 35.236%
19530478236

Pull #36

github

web-flow
Merge 874cab849 into 0e2836980
Pull Request #36: test: add unit tests for getMapOfOpaqueDeviceConfigForDevice

1075 of 2926 relevant lines covered (36.74%)

2.4 hits per line

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

74.48
/pkg/controller/resourcefiltercontroller.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
        "fmt"
22
        "strconv"
23
        "time"
24

25
        corev1 "k8s.io/api/core/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
        resourceapi "k8s.io/api/resource/v1"
41

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

47
const (
48
        resourceFilterSyncEventName = "resource-filter-sync"
49
)
50

51
// SriovResourceFilterReconciler reconciles SriovResourceFilter objects
52
type SriovResourceFilterReconciler struct {
53
        client.Client
54
        nodeName              string
55
        namespace             string
56
        currentResourceFilter *sriovdrav1alpha1.SriovResourceFilter
57
        log                   klog.Logger
58
        deviceStateManager    devicestate.DeviceState
59
}
60

61
// NewSriovResourceFilterReconciler creates a new SriovResourceFilterReconciler
62
func NewSriovResourceFilterReconciler(client client.Client, nodeName, namespace string, deviceStateManager devicestate.DeviceState) *SriovResourceFilterReconciler {
2✔
63
        return &SriovResourceFilterReconciler{
2✔
64
                Client:             client,
2✔
65
                deviceStateManager: deviceStateManager,
2✔
66
                nodeName:           nodeName,
2✔
67
                namespace:          namespace,
2✔
68
                log:                klog.Background().WithName("SriovResourceFilter"),
2✔
69
        }
2✔
70
}
2✔
71

72
// Reconcile handles the reconciliation of SriovResourceFilter resources
73
func (r *SriovResourceFilterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
8✔
74
        r.log.Info("Starting reconcile", "request", req.NamespacedName, "watchedNamespace", r.namespace)
8✔
75

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

88
        // List all SriovResourceFilter objects in the operator namespace
89
        resourceFilterList := &sriovdrav1alpha1.SriovResourceFilterList{}
7✔
90
        if err := r.List(ctx, resourceFilterList, client.InNamespace(r.namespace)); err != nil {
7✔
91
                r.log.Error(err, "Failed to list SriovResourceFilter objects", "namespace", r.namespace)
×
92
                return ctrl.Result{}, err
×
93
        }
×
94

95
        // Find matching resource filters for this node
96
        var matchingFilters []*sriovdrav1alpha1.SriovResourceFilter
7✔
97
        for i := range resourceFilterList.Items {
13✔
98
                filter := &resourceFilterList.Items[i]
6✔
99
                if r.matchesNodeSelector(node.Labels, filter.Spec.NodeSelector) {
12✔
100
                        matchingFilters = append(matchingFilters, filter)
6✔
101
                }
6✔
102
        }
103

104
        // Handle the results
105
        switch len(matchingFilters) {
7✔
106
        case 0:
2✔
107
                r.log.Info("No matching SriovResourceFilter found for node", "nodeName", r.nodeName)
2✔
108
                r.currentResourceFilter = nil
2✔
109
                // Clear resource filter from devices since no filter matches
2✔
110
                if err := r.applyResourceFilterToDevices(ctx); err != nil {
2✔
111
                        r.log.Error(err, "Failed to clear resource filter from devices")
×
112
                        return ctrl.Result{}, err
×
113
                }
×
114
        case 1:
4✔
115
                r.log.Info("Found matching SriovResourceFilter for node", "nodeName", r.nodeName, "filter", matchingFilters[0].Name)
4✔
116
                r.currentResourceFilter = matchingFilters[0]
4✔
117
                // Apply resource filter to devices
4✔
118
                if err := r.applyResourceFilterToDevices(ctx); err != nil {
4✔
119
                        r.log.Error(err, "Failed to apply resource filter to devices")
×
120
                        return ctrl.Result{}, err
×
121
                }
×
122
        default:
1✔
123
                // Multiple matches - log error and don't use any
1✔
124
                filterNames := make([]string, len(matchingFilters))
1✔
125
                for i, filter := range matchingFilters {
3✔
126
                        filterNames[i] = filter.Name
2✔
127
                }
2✔
128
                r.log.Error(fmt.Errorf("multiple SriovResourceFilter objects match node"),
1✔
129
                        "Multiple resource filters match node, ignoring all",
1✔
130
                        "nodeName", r.nodeName,
1✔
131
                        "matchingFilters", filterNames)
1✔
132
                r.currentResourceFilter = nil
1✔
133
        }
134

135
        return ctrl.Result{}, nil
7✔
136
}
137

138
// GetCurrentResourceFilter returns the currently active SriovResourceFilter for the node
139
func (r *SriovResourceFilterReconciler) GetCurrentResourceFilter() *sriovdrav1alpha1.SriovResourceFilter {
9✔
140
        return r.currentResourceFilter
9✔
141
}
9✔
142

143
// HasResourceFilter returns true if there is currently an active SriovResourceFilter for the node
144
func (r *SriovResourceFilterReconciler) HasResourceFilter() bool {
1✔
145
        return r.currentResourceFilter != nil
1✔
146
}
1✔
147

148
// GetConfigs returns the configs from the currently active SriovResourceFilter
149
// Returns nil if no resource filter is active
150
func (r *SriovResourceFilterReconciler) GetConfigs() []sriovdrav1alpha1.Config {
×
151
        if r.currentResourceFilter == nil {
×
152
                return nil
×
153
        }
×
154
        return r.currentResourceFilter.Spec.Configs
×
155
}
156

157
// GetResourceFilters returns all resource filters from all configs in the currently active SriovResourceFilter
158
// Returns nil if no resource filter is active
159
// Deprecated: Use GetConfigs() instead for better resource name handling
160
func (r *SriovResourceFilterReconciler) GetResourceFilters() []sriovdrav1alpha1.ResourceFilter {
×
161
        if r.currentResourceFilter == nil {
×
162
                return nil
×
163
        }
×
164
        var allFilters []sriovdrav1alpha1.ResourceFilter
×
165
        for _, config := range r.currentResourceFilter.Spec.Configs {
×
166
                allFilters = append(allFilters, config.ResourceFilters...)
×
167
        }
×
168
        return allFilters
×
169
}
170

171
// GetResourceNames returns all resource names from the currently active SriovResourceFilter
172
// Returns nil if no resource filter is active
173
func (r *SriovResourceFilterReconciler) GetResourceNames() []string {
7✔
174
        if r.currentResourceFilter == nil {
7✔
175
                return nil
×
176
        }
×
177
        var resourceNames []string
7✔
178
        for _, config := range r.currentResourceFilter.Spec.Configs {
14✔
179
                if config.ResourceName != "" {
14✔
180
                        resourceNames = append(resourceNames, config.ResourceName)
7✔
181
                }
7✔
182
        }
183
        return resourceNames
7✔
184
}
185

186
// matchesNodeSelector checks if node labels match the given selector
187
func (r *SriovResourceFilterReconciler) matchesNodeSelector(nodeLabels map[string]string, nodeSelector map[string]string) bool {
9✔
188
        if len(nodeSelector) == 0 {
15✔
189
                // Empty selector matches all nodes
6✔
190
                return true
6✔
191
        }
6✔
192

193
        selector := labels.Set(nodeSelector).AsSelector()
3✔
194
        return selector.Matches(labels.Set(nodeLabels))
3✔
195
}
196

197
// applyResourceFilterToDevices applies the current resource filter to devices
198
func (r *SriovResourceFilterReconciler) applyResourceFilterToDevices(ctx context.Context) error {
6✔
199
        deviceResourceMap := r.getFilteredDeviceResourceMap()
6✔
200
        return r.deviceStateManager.UpdateDeviceResourceNames(ctx, deviceResourceMap)
6✔
201
}
6✔
202

203
// getFilteredDeviceResourceMap returns a map of device name to resource name based on the current resource filter
204
func (r *SriovResourceFilterReconciler) getFilteredDeviceResourceMap() map[string]string {
8✔
205
        deviceResourceMap := make(map[string]string)
8✔
206

8✔
207
        // If no resource filter is active, return empty map (clears resource names)
8✔
208
        if r.currentResourceFilter == nil {
11✔
209
                r.log.V(2).Info("No active resource filter, clearing all resource names")
3✔
210
                return deviceResourceMap
3✔
211
        }
3✔
212

213
        // Get all allocatable devices from device state manager
214
        allocatableDevices := r.deviceStateManager.GetAllocatableDevices()
5✔
215

5✔
216
        r.log.V(2).Info("Applying resource filter to devices",
5✔
217
                "filterName", r.currentResourceFilter.Name,
5✔
218
                "totalConfigs", len(r.currentResourceFilter.Spec.Configs),
5✔
219
                "totalDevices", len(allocatableDevices))
5✔
220

5✔
221
        // Iterate through each config and apply its resource filters to devices
5✔
222
        for _, config := range r.currentResourceFilter.Spec.Configs {
12✔
223
                if config.ResourceName == "" {
8✔
224
                        r.log.V(2).Info("Skipping config with empty resource name", "filterName", r.currentResourceFilter.Name)
1✔
225
                        continue
1✔
226
                }
227

228
                r.log.V(3).Info("Processing config",
6✔
229
                        "filterName", r.currentResourceFilter.Name,
6✔
230
                        "resourceName", config.ResourceName,
6✔
231
                        "filtersCount", len(config.ResourceFilters))
6✔
232

6✔
233
                // Apply this config's resource filters to devices
6✔
234
                for deviceName, device := range allocatableDevices {
18✔
235
                        // Skip device if it's already assigned a resource name
12✔
236
                        if _, exists := deviceResourceMap[deviceName]; exists {
14✔
237
                                continue
2✔
238
                        }
239

240
                        if r.deviceMatchesFilters(device, config.ResourceFilters) {
20✔
241
                                deviceResourceMap[deviceName] = config.ResourceName
10✔
242
                                r.log.V(3).Info("Device matches config filter",
10✔
243
                                        "deviceName", deviceName,
10✔
244
                                        "resourceName", config.ResourceName,
10✔
245
                                        "filterName", r.currentResourceFilter.Name)
10✔
246
                        }
10✔
247
                }
248
        }
249

250
        r.log.Info("Resource filter applied",
5✔
251
                "filterName", r.currentResourceFilter.Name,
5✔
252
                "matchingDevices", len(deviceResourceMap),
5✔
253
                "totalDevices", len(allocatableDevices))
5✔
254

5✔
255
        return deviceResourceMap
5✔
256
}
257

258
// deviceMatchesFilters checks if a device matches any of the provided resource filters
259
func (r *SriovResourceFilterReconciler) deviceMatchesFilters(device resourceapi.Device, filters []sriovdrav1alpha1.ResourceFilter) bool {
10✔
260
        // If no filters are specified, match all devices
10✔
261
        if len(filters) == 0 {
18✔
262
                return true
8✔
263
        }
8✔
264

265
        // Device matches if it matches ANY of the filters (OR logic)
266
        for _, filter := range filters {
4✔
267
                if r.deviceMatchesFilter(device, filter) {
4✔
268
                        return true
2✔
269
                }
2✔
270
        }
271

272
        return false
×
273
}
274

275
// deviceMatchesFilter checks if a device matches a specific resource filter
276
func (r *SriovResourceFilterReconciler) deviceMatchesFilter(device resourceapi.Device, filter sriovdrav1alpha1.ResourceFilter) bool {
10✔
277
        // Check vendor IDs
10✔
278
        if len(filter.Vendors) > 0 {
14✔
279
                vendorAttr, exists := device.Attributes[consts.AttributeVendorID]
4✔
280
                if !exists || vendorAttr.StringValue == nil {
4✔
281
                        return false
×
282
                }
×
283
                if !r.stringSliceContains(filter.Vendors, *vendorAttr.StringValue) {
5✔
284
                        return false
1✔
285
                }
1✔
286
        }
287

288
        // Check device IDs
289
        if len(filter.Devices) > 0 {
11✔
290
                deviceAttr, exists := device.Attributes[consts.AttributeDeviceID]
2✔
291
                if !exists || deviceAttr.StringValue == nil {
2✔
292
                        return false
×
293
                }
×
294
                if !r.stringSliceContains(filter.Devices, *deviceAttr.StringValue) {
3✔
295
                        return false
1✔
296
                }
1✔
297
        }
298

299
        // Check PCI addresses
300
        if len(filter.PciAddresses) > 0 {
10✔
301
                pciAttr, exists := device.Attributes[consts.AttributePciAddress]
2✔
302
                if !exists || pciAttr.StringValue == nil {
2✔
303
                        return false
×
304
                }
×
305
                if !r.stringSliceContains(filter.PciAddresses, *pciAttr.StringValue) {
3✔
306
                        return false
1✔
307
                }
1✔
308
        }
309

310
        // Check PF names
311
        if len(filter.PfNames) > 0 {
9✔
312
                pfAttr, exists := device.Attributes[consts.AttributePFName]
2✔
313
                if !exists || pfAttr.StringValue == nil {
2✔
314
                        return false
×
315
                }
×
316
                if !r.stringSliceContains(filter.PfNames, *pfAttr.StringValue) {
3✔
317
                        return false
1✔
318
                }
1✔
319
        }
320

321
        // Check root devices (parent PCI addresses)
322
        if len(filter.RootDevices) > 0 {
8✔
323
                rootAttr, exists := device.Attributes[consts.AttributeParentPciAddress]
2✔
324
                if !exists || rootAttr.StringValue == nil {
2✔
325
                        return false
×
326
                }
×
327
                if !r.stringSliceContains(filter.RootDevices, *rootAttr.StringValue) {
3✔
328
                        return false
1✔
329
                }
1✔
330
        }
331

332
        // Check NUMA nodes
333
        if len(filter.NumaNodes) > 0 {
7✔
334
                numaAttr, exists := device.Attributes[consts.AttributeNumaNode]
2✔
335
                if !exists || numaAttr.IntValue == nil {
2✔
336
                        return false
×
337
                }
×
338
                numaStr := strconv.FormatInt(*numaAttr.IntValue, 10)
2✔
339
                if !r.stringSliceContains(filter.NumaNodes, numaStr) {
3✔
340
                        return false
1✔
341
                }
1✔
342
        }
343

344
        // Check drivers - this is more complex as we need to check the current driver binding
345
        // For now, we'll skip this check as it would require additional system calls
346
        // TODO: Implement driver checking if needed
347
        if len(filter.Drivers) > 0 {
4✔
348
                r.log.V(3).Info("Driver filtering not yet implemented", "deviceName", device.Name)
×
349
        }
×
350

351
        // All specified filters match
352
        return true
4✔
353
}
354

355
// stringSliceContains checks if a slice contains a specific string
356
func (r *SriovResourceFilterReconciler) stringSliceContains(slice []string, item string) bool {
16✔
357
        for _, s := range slice {
34✔
358
                if s == item {
27✔
359
                        return true
9✔
360
                }
9✔
361
        }
362
        return false
7✔
363
}
364

365
// SetupWithManager sets up the controller with the Manager.
366
func (r *SriovResourceFilterReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
367
        qHandler := func(q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
6✔
368
                q.AddAfter(reconcile.Request{NamespacedName: types.NamespacedName{
5✔
369
                        Namespace: r.namespace,
5✔
370
                        Name:      resourceFilterSyncEventName,
5✔
371
                }}, time.Second)
5✔
372
        }
5✔
373

374
        delayedEventHandler := handler.Funcs{
1✔
375
                CreateFunc: func(_ context.Context, e event.TypedCreateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
4✔
376
                        r.log.Info("Enqueuing sync for create event",
3✔
377
                                "resource", e.Object.GetName(),
3✔
378
                                "type", e.Object.GetObjectKind().GroupVersionKind().String())
3✔
379
                        qHandler(w)
3✔
380
                },
3✔
381
                UpdateFunc: func(_ context.Context, e event.TypedUpdateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
×
382
                        r.log.Info("Enqueuing sync for update event",
×
383
                                "resource", e.ObjectNew.GetName(),
×
384
                                "type", e.ObjectNew.GetObjectKind().GroupVersionKind().String())
×
385
                        qHandler(w)
×
386
                },
×
387
                DeleteFunc: func(_ context.Context, e event.TypedDeleteEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
2✔
388
                        r.log.Info("Enqueuing sync for delete event",
2✔
389
                                "resource", e.Object.GetName(),
2✔
390
                                "type", e.Object.GetObjectKind().GroupVersionKind().String())
2✔
391
                        qHandler(w)
2✔
392
                },
2✔
393
                GenericFunc: func(_ context.Context, e event.TypedGenericEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
×
394
                        r.log.Info("Enqueuing sync for generic event",
×
395
                                "resource", e.Object.GetName(),
×
396
                                "type", e.Object.GetObjectKind().GroupVersionKind().String())
×
397
                        qHandler(w)
×
398
                },
×
399
        }
400

401
        // Node event handler - we care about node label changes
402
        nodeEventHandler := handler.Funcs{
1✔
403
                CreateFunc: func(_ context.Context, e event.TypedCreateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
1✔
404
                        // Only care about our node
×
405
                        if e.Object.GetName() == r.nodeName {
×
406
                                r.log.Info("Enqueuing sync for node create event", "node", e.Object.GetName())
×
407
                                qHandler(w)
×
408
                        }
×
409
                },
410
                UpdateFunc: func(_ context.Context, e event.TypedUpdateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
×
411
                        // Only care about our node and only if labels changed
×
412
                        if e.ObjectNew.GetName() == r.nodeName {
×
413
                                oldLabels := e.ObjectOld.GetLabels()
×
414
                                newLabels := e.ObjectNew.GetLabels()
×
415
                                if !labels.Equals(oldLabels, newLabels) {
×
416
                                        r.log.Info("Enqueuing sync for node label change event", "node", e.ObjectNew.GetName())
×
417
                                        qHandler(w)
×
418
                                }
×
419
                        }
420
                },
421
                DeleteFunc: func(_ context.Context, e event.TypedDeleteEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
×
422
                        // Only care about our node
×
423
                        if e.Object.GetName() == r.nodeName {
×
424
                                r.log.Info("Enqueuing sync for node delete event", "node", e.Object.GetName())
×
425
                                qHandler(w)
×
426
                        }
×
427
                },
428
        }
429

430
        // Send initial sync event to trigger reconcile when controller is started
431
        var eventChan = make(chan event.GenericEvent, 1)
1✔
432
        eventChan <- event.GenericEvent{Object: &sriovdrav1alpha1.SriovResourceFilter{
1✔
433
                ObjectMeta: metav1.ObjectMeta{Name: resourceFilterSyncEventName, Namespace: r.namespace}}}
1✔
434
        close(eventChan)
1✔
435

1✔
436
        // Create predicate to filter SriovResourceFilter events to only the operator namespace
1✔
437
        namespacePredicate := predicate.NewPredicateFuncs(func(obj client.Object) bool {
15✔
438
                return obj.GetNamespace() == r.namespace
14✔
439
        })
14✔
440

441
        // Set up PartialObjectMetadata for Node resources
442
        nodeMetadata := &metav1.PartialObjectMetadata{}
1✔
443
        nodeMetadata.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Node"))
1✔
444

1✔
445
        return ctrl.NewControllerManagedBy(mgr).
1✔
446
                For(&sriovdrav1alpha1.SriovResourceFilter{}).
1✔
447
                Watches(nodeMetadata, nodeEventHandler).
1✔
448
                Watches(&sriovdrav1alpha1.SriovResourceFilter{}, delayedEventHandler).
1✔
449
                WithEventFilter(namespacePredicate).
1✔
450
                WatchesRawSource(source.Channel(eventChan, &handler.EnqueueRequestForObject{})).
1✔
451
                Complete(r)
1✔
452
}
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

© 2025 Coveralls, Inc