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

k8snetworkplumbingwg / dra-driver-sriov / 22661858861

04 Mar 2026 08:48AM UTC coverage: 45.495% (-0.06%) from 45.558%
22661858861

Pull #74

github

web-flow
Merge 8f76ffacb into 8549f85a9
Pull Request #74: fix: use lowercase github owner for helm

1434 of 3152 relevant lines covered (45.49%)

3.17 hits per line

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

74.38
/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
        "time"
23

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

39
        resourceapi "k8s.io/api/resource/v1"
40

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

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

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

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

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

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

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

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

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

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

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

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

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

156
// GetResourceFilters returns all resource filters from all configs in the currently active SriovResourceFilter
157
// Returns nil if no resource filter is active
158
//
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 {
9✔
277
        // Check vendor IDs
9✔
278
        if len(filter.Vendors) > 0 {
13✔
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 {
10✔
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 {
9✔
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 {
8✔
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, e.g., "0000:00:00.0")
322
        // This filters by immediate parent device for granular filtering
323
        if len(filter.RootDevices) > 0 {
7✔
324
                parentAttr, exists := device.Attributes[consts.AttributeParentPciAddress]
2✔
325
                if !exists || parentAttr.StringValue == nil {
2✔
326
                        return false
×
327
                }
×
328
                if !r.stringSliceContains(filter.RootDevices, *parentAttr.StringValue) {
3✔
329
                        return false
1✔
330
                }
1✔
331
        }
332

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

340
        // All specified filters match
341
        return true
4✔
342
}
343

344
// stringSliceContains checks if a slice contains a specific string
345
func (r *SriovResourceFilterReconciler) stringSliceContains(slice []string, item string) bool {
14✔
346
        for _, s := range slice {
30✔
347
                if s == item {
24✔
348
                        return true
8✔
349
                }
8✔
350
        }
351
        return false
6✔
352
}
353

354
// SetupWithManager sets up the controller with the Manager.
355
func (r *SriovResourceFilterReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
356
        qHandler := func(q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
6✔
357
                q.AddAfter(reconcile.Request{NamespacedName: types.NamespacedName{
5✔
358
                        Namespace: r.namespace,
5✔
359
                        Name:      resourceFilterSyncEventName,
5✔
360
                }}, time.Second)
5✔
361
        }
5✔
362

363
        delayedEventHandler := handler.Funcs{
1✔
364
                CreateFunc: func(_ context.Context, e event.TypedCreateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
4✔
365
                        r.log.Info("Enqueuing sync for create event",
3✔
366
                                "resource", e.Object.GetName(),
3✔
367
                                "type", e.Object.GetObjectKind().GroupVersionKind().String())
3✔
368
                        qHandler(w)
3✔
369
                },
3✔
370
                UpdateFunc: func(_ context.Context, e event.TypedUpdateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
×
371
                        r.log.Info("Enqueuing sync for update event",
×
372
                                "resource", e.ObjectNew.GetName(),
×
373
                                "type", e.ObjectNew.GetObjectKind().GroupVersionKind().String())
×
374
                        qHandler(w)
×
375
                },
×
376
                DeleteFunc: func(_ context.Context, e event.TypedDeleteEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
2✔
377
                        r.log.Info("Enqueuing sync for delete event",
2✔
378
                                "resource", e.Object.GetName(),
2✔
379
                                "type", e.Object.GetObjectKind().GroupVersionKind().String())
2✔
380
                        qHandler(w)
2✔
381
                },
2✔
382
                GenericFunc: func(_ context.Context, e event.TypedGenericEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
×
383
                        r.log.Info("Enqueuing sync for generic event",
×
384
                                "resource", e.Object.GetName(),
×
385
                                "type", e.Object.GetObjectKind().GroupVersionKind().String())
×
386
                        qHandler(w)
×
387
                },
×
388
        }
389

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

419
        // Send initial sync event to trigger reconcile when controller is started
420
        var eventChan = make(chan event.GenericEvent, 1)
1✔
421
        eventChan <- event.GenericEvent{Object: &sriovdrav1alpha1.SriovResourceFilter{
1✔
422
                ObjectMeta: metav1.ObjectMeta{Name: resourceFilterSyncEventName, Namespace: r.namespace}}}
1✔
423
        close(eventChan)
1✔
424

1✔
425
        // Create predicate to filter SriovResourceFilter events to only the operator namespace
1✔
426
        namespacePredicate := predicate.NewPredicateFuncs(func(obj client.Object) bool {
15✔
427
                return obj.GetNamespace() == r.namespace
14✔
428
        })
14✔
429

430
        // Set up PartialObjectMetadata for Node resources
431
        nodeMetadata := &metav1.PartialObjectMetadata{}
1✔
432
        nodeMetadata.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Node"))
1✔
433

1✔
434
        return ctrl.NewControllerManagedBy(mgr).
1✔
435
                For(&sriovdrav1alpha1.SriovResourceFilter{}).
1✔
436
                Watches(nodeMetadata, nodeEventHandler).
1✔
437
                Watches(&sriovdrav1alpha1.SriovResourceFilter{}, delayedEventHandler).
1✔
438
                WithEventFilter(namespacePredicate).
1✔
439
                WatchesRawSource(source.Channel(eventChan, &handler.EnqueueRequestForObject{})).
1✔
440
                Complete(r)
1✔
441
}
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