diff --git a/deployments/multus-daemonset-crio.yml b/deployments/multus-daemonset-crio.yml index e3c28d4ff..bcd40ebe2 100644 --- a/deployments/multus-daemonset-crio.yml +++ b/deployments/multus-daemonset-crio.yml @@ -70,7 +70,18 @@ rules: - pods/status verbs: - get + - list - update + - watch + - apiGroups: + - "resource.k8s.io" + resources: + - resourceclaims + - resourceclaims/status + - resourceslices + verbs: + - get + - list - apiGroups: - "" - events.k8s.io diff --git a/deployments/multus-daemonset-thick.yml b/deployments/multus-daemonset-thick.yml index 1d9b12cc3..4840e869e 100644 --- a/deployments/multus-daemonset-thick.yml +++ b/deployments/multus-daemonset-thick.yml @@ -73,6 +73,15 @@ rules: - list - update - watch + - apiGroups: + - "resource.k8s.io" + resources: + - resourceclaims + - resourceclaims/status + - resourceslices + verbs: + - get + - list - apiGroups: - "" - events.k8s.io diff --git a/deployments/multus-daemonset.yml b/deployments/multus-daemonset.yml index 29cdcc4c7..dc8ae9cf7 100644 --- a/deployments/multus-daemonset.yml +++ b/deployments/multus-daemonset.yml @@ -70,7 +70,18 @@ rules: - pods/status verbs: - get + - list - update + - watch + - apiGroups: + - "resource.k8s.io" + resources: + - resourceclaims + - resourceclaims/status + - resourceslices + verbs: + - get + - list - apiGroups: - "" - events.k8s.io diff --git a/docs/how-to-use.md b/docs/how-to-use.md index b4cdfc426..7bd696926 100644 --- a/docs/how-to-use.md +++ b/docs/how-to-use.md @@ -645,112 +645,144 @@ If you wish to have auto configuration use the `readinessindicatorfile` in the c ### Run pod with network annotation and Dynamic Resource Allocation driver -> :warning: Dynamic Resource Allocation (DRA) is [currently an alpha](https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/), -> and is subject to change. Please consider this functionality as a preview. The architecture and usage of DRA in -> Multus CNI may change in the future as this technology matures. -> -> The current DRA integration is based on the DRA API for Kubernetes 1.26 to 1.30. With Kubernetes 1.31, the DRA API -> will change and multus doesn't integrate with the new API yet. -Dynamic Resource Allocation is alternative mechanism to device plugin which allows to requests pod and container -resources. +Dynamic Resource Allocation is an alternative mechanism to device plugin which allows pods to request pod and container +resources dynamically. -The following sections describe how to use DRA with multus and NVIDIA DRA driver. Other DRA networking driver vendors -should follow similar concepts to make use of multus DRA support. +The following sections describe how to use DRA with Multus. DRA networking driver vendors should follow similar +concepts to make use of Multus DRA support. #### Prerequisite -1. Kubernetes 1.27 -2. Container Runtime with CDI support enabled -3. Kubernetes runtime-config=resource.k8s.io/v1alpha2 -4. Kubernetes feature-gates=DynamicResourceAllocation=True,KubeletPodResourcesDynamicResources=true +1. Kubernetes 1.34+ #### Install DRA driver -The current example uses NVIDIA DRA driver for networking. This DRA driver is not publicly available. An alternative to -this DRA driver is available at [dra-example-driver](https://github.com/kubernetes-sigs/dra-example-driver). +You need to install a DRA driver that provides network devices. For example, you can use the [SR-IOV DRA](https://github.com/k8snetworkplumbingwg/dra-driver-sriov) driver or +other DRA networking drivers. Refer to your DRA driver documentation for installation instructions. -#### Create dynamic resource class with NVIDIA network DRA driver +The DRA driver MUST expose the following attributes on each allocated **network** device in `ResourceSlice`: +- `k8s.cni.cncf.io/deviceID`: device ID that Multus passes to the CNI plugin. +- `k8s.cni.cncf.io/resourceName`: **must exactly match** the value you put on the NetworkAttachmentDefinition + `k8s.v1.cni.cncf.io/resourceName` annotation (same style as classic extended resources, e.g. `intel.com/sriov_vf`). -The `ResourceClass` defines the resource pool of `sf-pool-1`. +Devices that lack `k8s.cni.cncf.io/deviceID` or `k8s.cni.cncf.io/resourceName` are **silently skipped** in the +normal claims path — this is intentional so that mixed pods with both network and non-network DRA claims (e.g. a +GPU claim alongside an SR-IOV claim) work correctly. The existing kubelet / device-plugin resource-map entries for +such pods are preserved unchanged. -``` -# Execute following command at Kubernetes master -cat < 0 { + logging.Warningf( + "GetPodResourceMap: claim %s had no allocation results mapped for Multus (skipping this claim; existing kubelet/device-plugin map entries are kept). "+ + "Fix DRA ResourceSlices or Multus attributes if this claim should contribute to CNI.", + claimName) + continue + } + logging.Debugf("GetPodResourceMap: successfully processed resource claim %s", claimName) + } + + if pod.Status.ExtendedResourceClaimStatus != nil { + if err := d.processExtendedResourceClaimStatus(ctx, nodeName, pod, resourceMap); err != nil { + return err + } + } + + types.SortDeviceIDs(resourceMap) + logging.Verbosef("GetPodResourceMap: successfully processed all DRA resources for pod %s/%s, total resources: %d", + pod.Namespace, pod.Name, len(resourceMap)) + return nil +} + +// processExtendedResourceClaimStatus fills the resource map for pods that use +// the extended resource feature gate (pod.Status.ExtendedResourceClaimStatus). +// Keys come from requestMappings[].resourceName (same as NAD annotation). +func (d *draClient) processExtendedResourceClaimStatus(ctx context.Context, nodeName string, pod *v1.Pod, resourceMap map[string]*types.ResourceInfo) error { + extStatus := pod.Status.ExtendedResourceClaimStatus + claimName := extStatus.ResourceClaimName + logging.Debugf("GetPodResourceMap: processing extended resource claim: %s/%s", pod.Namespace, claimName) + + resourceClaim, err := d.getOrFetchResourceClaim(ctx, pod.Namespace, claimName) + if err != nil { + logging.Errorf("GetPodResourceMap: failed to get extended resource claim %s/%s: %v", pod.Namespace, claimName, err) + return err + } + + if resourceClaim.Status.Allocation == nil || resourceClaim.Status.Allocation.Devices.Results == nil { + logging.Errorf("GetPodResourceMap: claim %s has no device allocation", claimName) + return fmt.Errorf("claim %s has no device allocation", claimName) + } + + resultsByRequest := make(map[string][]resourcev1api.DeviceRequestAllocationResult) + for _, result := range resourceClaim.Status.Allocation.Devices.Results { + resultsByRequest[result.Request] = append(resultsByRequest[result.Request], result) + } + + for _, mapping := range extStatus.RequestMappings { + results, ok := resultsByRequest[mapping.RequestName] + if !ok || len(results) == 0 { + logging.Errorf("GetPodResourceMap: extended resource request %s not found in claim %s", mapping.RequestName, claimName) + return fmt.Errorf("request %s not found in claim %s", mapping.RequestName, claimName) + } + + resourceMapKey := mapping.ResourceName + for _, result := range results { + info, err := d.getDeviceInfo(ctx, nodeName, result) + if err != nil { + logging.Errorf("GetPodResourceMap: failed to get device info for extended resource claim %s request %s: %v", claimName, mapping.RequestName, err) + return err + } + + if info.ResourceName == "" { + resErr := fmt.Errorf("device %s missing required attribute %s (must match NAD k8s.v1.cni.cncf.io/resourceName and extended mapping %q)", + result.Device, multusResourceNameAttr, resourceMapKey) + logging.Errorf("GetPodResourceMap: %v", resErr) + return resErr + } + if info.ResourceName != mapping.ResourceName { + resErr := fmt.Errorf("device %s: %s is %q but extended resource mapping for request %q is %q", + result.Device, multusResourceNameAttr, info.ResourceName, mapping.RequestName, mapping.ResourceName) + logging.Errorf("GetPodResourceMap: %v", resErr) + return resErr + } + + if rInfo, ok := resourceMap[resourceMapKey]; ok { + rInfo.DeviceIDs = append(rInfo.DeviceIDs, info.DeviceID) + logging.Debugf("GetPodResourceMap: appended device ID %s to extended resource map entry %s", info.DeviceID, resourceMapKey) + } else { + resourceMap[resourceMapKey] = &types.ResourceInfo{DeviceIDs: []string{info.DeviceID}} + logging.Debugf("GetPodResourceMap: created new extended resource map entry %s with device ID %s", resourceMapKey, info.DeviceID) + } + } + } + + logging.Debugf("GetPodResourceMap: successfully processed extended resource claim %s", claimName) + return nil +} + +// ensureDriverCachePopulated lists ResourceSlices for the given node and driver (using server-side +// field selectors) and extracts only the two attributes Multus needs into deviceInfoCache. +// Full ResourceSlice objects are discarded after extraction, keeping memory usage minimal. +// Subsequent calls for the same node/driver combination are no-ops. +func (d *draClient) ensureDriverCachePopulated(ctx context.Context, nodeName, driverName string) error { + populatedKey := nodeName + "/" + driverName + if d.populatedDrivers[populatedKey] { + return nil + } + + // driverName is always non-empty (Driver is +required in DeviceRequestAllocationResult). + // Always filter by driver to avoid loading unrelated slices into the cache. + // Also filter by node when available to further scope the list. + listOptions := metav1.ListOptions{ + FieldSelector: fmt.Sprintf("%s=%s", resourcev1api.ResourceSliceSelectorDriver, driverName), + } + if nodeName != "" { + listOptions.FieldSelector = fmt.Sprintf("%s=%s,%s=%s", + resourcev1api.ResourceSliceSelectorNodeName, nodeName, + resourcev1api.ResourceSliceSelectorDriver, driverName) + } + + logging.Debugf("ensureDriverCachePopulated: listing ResourceSlices (fieldSelector=%q)", listOptions.FieldSelector) + slices, err := d.client.ResourceSlices().List(ctx, listOptions) + if err != nil { + logging.Errorf("ensureDriverCachePopulated: failed to list resource slices: %v", err) + return err + } + logging.Debugf("ensureDriverCachePopulated: listed %d ResourceSlice(s) for node=%q driver=%q", len(slices.Items), nodeName, driverName) + + for i := range slices.Items { + slice := &slices.Items[i] + driverPool := fmt.Sprintf("%s/%s", slice.Spec.Driver, slice.Spec.Pool.Name) + for _, device := range slice.Spec.Devices { + key := deviceInfoCacheKey{driverPool: driverPool, deviceName: device.Name} + info := &deviceInfo{} + if attr, ok := device.Attributes[multusDeviceIDAttr]; ok && attr.StringValue != nil { + info.DeviceID = *attr.StringValue + } + if attr, ok := device.Attributes[multusResourceNameAttr]; ok && attr.StringValue != nil { + info.ResourceName = *attr.StringValue + } + if info.DeviceID != "" { + d.deviceInfoCache[key] = info + } + } + } + + d.populatedDrivers[populatedKey] = true + return nil +} + +// getDeviceInfo returns the cached deviceInfo for the device named in result. +// On the first call for a given driver it triggers ensureDriverCachePopulated, +// which lists ResourceSlices scoped to nodeName and result.Driver via server-side +// field selectors and extracts only the two attributes Multus needs. Subsequent +// calls for the same driver are O(1) map lookups with no API traffic. +// Returns errDeviceNotInAnySlice (wrapped) if the device is absent from the cache +// or lacks a deviceID attribute; callers in the normal claims path treat this as +// a skip, while the extended-resource path treats it as a hard error. +func (d *draClient) getDeviceInfo(ctx context.Context, nodeName string, result resourcev1api.DeviceRequestAllocationResult) (*deviceInfo, error) { + driverPool := fmt.Sprintf("%s/%s", result.Driver, result.Pool) + logging.Debugf("getDeviceInfo: looking up device for driver/pool: %s, device: %s", driverPool, result.Device) + + if err := d.ensureDriverCachePopulated(ctx, nodeName, result.Driver); err != nil { + return nil, err + } + + key := deviceInfoCacheKey{driverPool: driverPool, deviceName: result.Device} + info, ok := d.deviceInfoCache[key] + if !ok { + notFoundErr := fmt.Errorf("%w: device %s not found for claim resource %s/%s in any matching resource slice", + errDeviceNotInAnySlice, result.Device, result.Driver, result.Pool) + logging.Errorf("getDeviceInfo: %v", notFoundErr) + return nil, notFoundErr + } + + if info.DeviceID == "" { + logging.Warningf( + "getDeviceInfo: device %q (driver %q, pool %q) has no %q in ResourceSlice; skipping allocation result", + result.Device, result.Driver, result.Pool, multusDeviceIDAttr) + return nil, fmt.Errorf("%w: device %q present in slice but missing %q", + errDeviceNotInAnySlice, result.Device, multusDeviceIDAttr) + } + + logging.Verbosef("getDeviceInfo: successfully retrieved info for device %s (driver/pool: %s): deviceID=%s, resourceName=%s", + result.Device, driverPool, info.DeviceID, info.ResourceName) + return info, nil +} diff --git a/pkg/draclient/draclient_suite_test.go b/pkg/draclient/draclient_suite_test.go new file mode 100644 index 000000000..651862c1b --- /dev/null +++ b/pkg/draclient/draclient_suite_test.go @@ -0,0 +1,29 @@ +// Copyright (c) 2025 Multus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package draclient + +// disable dot-imports only for testing +//revive:disable:dot-imports +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestDRAClient(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "draclient") +} diff --git a/pkg/draclient/draclient_test.go b/pkg/draclient/draclient_test.go new file mode 100644 index 000000000..83f822b23 --- /dev/null +++ b/pkg/draclient/draclient_test.go @@ -0,0 +1,1979 @@ +// Copyright (c) 2025 Multus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package draclient + +// disable dot-imports only for testing +//revive:disable:dot-imports +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + v1 "k8s.io/api/core/v1" + resourcev1api "k8s.io/api/resource/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + k8sTypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/fake" + + "gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/types" +) + +var _ = Describe("DRA Client operations", func() { + + Describe("NewClient", func() { + It("should create a new DRA client successfully", func() { + fakeClient := fake.NewClientset() + client := NewClient(fakeClient.ResourceV1()) + Expect(client).NotTo(BeNil()) + }) + }) + + Describe("GetPodResourceMap", func() { + var ( + fakeClient *fake.Clientset + draClient ClientInterface + ) + + BeforeEach(func() { + fakeClient = fake.NewClientset() + draClient = NewClient(fakeClient.ResourceV1()) + }) + + Context("when pod has no resource claims", func() { + It("should return empty resource map without error", func() { + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: k8sTypes.UID("test-uid"), + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{}, + }, + } + + resourceMap := make(map[string]*types.ResourceInfo) + err := draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + Expect(resourceMap).To(BeEmpty()) + }) + }) + + Context("when resource claim exists with valid device allocation", func() { + It("should successfully populate resource map with device IDs", func() { + claimName := "test-claim" + deviceName := "device-1" + driverName := "test-driver.example.com" + poolName := "test-pool" + requestName := "gpu" + deviceID := "pci:0000:00:01.0" + mapKey := "intel.com/gpu-vf" + + // Create ResourceSlice with device + deviceIDValue := deviceID + mapKeyValue := mapKey + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource-slice", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: deviceName, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: { + StringValue: &deviceIDValue, + }, + multusResourceNameAttr: { + StringValue: &mapKeyValue, + }, + }, + }, + }, + }, + } + + // Create ResourceClaim with allocation + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: "default", + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: requestName, + Driver: driverName, + Pool: poolName, + Device: deviceName, + }, + }, + }, + }, + }, + } + + // Create pod with resource claim status. + // Spec.NodeName is set so ensureDriverCachePopulated takes the + // node-scoped selector branch (spec.nodeName + spec.driver). + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: k8sTypes.UID("test-uid"), + }, + Spec: v1.PodSpec{ + NodeName: "test-node", + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + { + Name: claimName, // spec ref name (pod.spec.resourceClaims[].name) + ResourceClaimName: &claimNamePtr, + }, + }, + }, + } + + // Add objects to fake client + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // Execute + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + + Expect(resourceMap).To(HaveKey(mapKey)) + Expect(resourceMap[mapKey].DeviceIDs).To(Equal([]string{deviceID})) + }) + + It("should not include devices from a different node when the pod has a nodeName set", func() { + // This test verifies node-scoping: when nodeName is non-empty the code + // uses a spec.nodeName/spec.driver field selector. The fake API server + // does not enforce server-side field selectors, but we can still confirm + // that only the device actually allocated to the pod's claim appears in + // the result — devices from a slice that is on a different node but + // shares the same driver are a no-op because the claim does not reference + // their device/pool. + claimName := "node-scoped-claim" + driverName := "test-driver.example.com" + poolName := "test-pool" + deviceName := "device-1" + deviceID := "pci:0000:00:01.0" + mapKey := "intel.com/sriov-vf" + + deviceIDValue := deviceID + mapKeyValue := mapKey + podNodeName := "test-node" + otherNodeNameVal := "other-node" + // Slice for the pod's node. + sliceOnPodNode := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "slice-test-node"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + NodeName: &podNodeName, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + {Name: deviceName, Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &deviceIDValue}, + multusResourceNameAttr: {StringValue: &mapKeyValue}, + }}, + }, + }, + } + + otherDeviceID := "pci:0000:00:99.0" + otherDeviceIDValue := otherDeviceID + otherMapKeyValue := mapKey + // Slice for a different node — should not produce entries for this pod's claim. + sliceOnOtherNode := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "slice-other-node"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + NodeName: &otherNodeNameVal, + Pool: resourcev1api.ResourcePool{Name: "other-pool", ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + {Name: "other-device", Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &otherDeviceIDValue}, + multusResourceNameAttr: {StringValue: &otherMapKeyValue}, + }}, + }, + }, + } + + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "scoped-pod", Namespace: "default"}, + Spec: v1.PodSpec{NodeName: "test-node"}, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + {Name: claimName, ResourceClaimName: &claimNamePtr}, + }, + }, + } + + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claimName, Namespace: "default"}, + Status: resourcev1api.ResourceClaimStatus{Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: "sriov", Driver: driverName, Pool: poolName, Device: deviceName}, + }}, + }}, + } + + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), sliceOnPodNode, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), sliceOnOtherNode, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + + // Only the device allocated by the claim (on "test-node") must appear. + Expect(resourceMap).To(HaveKey(mapKey)) + Expect(resourceMap[mapKey].DeviceIDs).To(ConsistOf(deviceID)) + // The other-node device ID must not be present. + Expect(resourceMap[mapKey].DeviceIDs).NotTo(ContainElement(otherDeviceID)) + }) + }) + + Context("when multiple devices are allocated to the same claim/request", func() { + It("should append all device IDs to the resource map", func() { + claimName := "multi-device-claim" + driverName := "test-driver.example.com" + poolName := "test-pool" + requestName := "gpu" + device1Name := "device-1" + device2Name := "device-2" + deviceID1 := "pci:0000:00:01.0" + deviceID2 := "pci:0000:00:02.0" + mapKey := "example.com/multi-gpu" + + // Create ResourceSlice with multiple devices (same resourceName → one NAD, multiple device IDs) + deviceID1Value := deviceID1 + deviceID2Value := deviceID2 + mapKeyValue := mapKey + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource-slice", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: device1Name, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: { + StringValue: &deviceID1Value, + }, + multusResourceNameAttr: {StringValue: &mapKeyValue}, + }, + }, + { + Name: device2Name, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: { + StringValue: &deviceID2Value, + }, + multusResourceNameAttr: {StringValue: &mapKeyValue}, + }, + }, + }, + }, + } + + // Create ResourceClaim with multiple device allocations + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: "default", + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: requestName, + Driver: driverName, + Pool: poolName, + Device: device1Name, + }, + { + Request: requestName, + Driver: driverName, + Pool: poolName, + Device: device2Name, + }, + }, + }, + }, + }, + } + + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: k8sTypes.UID("test-uid"), + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + { + Name: claimName, + ResourceClaimName: &claimNamePtr, + }, + }, + }, + } + + // Add objects to fake client + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // Execute + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + + Expect(resourceMap).To(HaveKey(mapKey)) + Expect(resourceMap[mapKey].DeviceIDs).To(ConsistOf(deviceID1, deviceID2)) + }) + }) + + Context("when one claim allocates multiple devices with distinct resourceName attributes", func() { + It("should populate separate resource map entries per network", func() { + claimName := "dual-net-claim" + driverName := "test-driver.example.com" + poolName := "test-pool" + device1Name := "dev-net-a" + device2Name := "dev-net-b" + deviceID1 := "pci:0000:00:01.0" + deviceID2 := "pci:0000:00:02.0" + keyA := "vendor.com/net-a" + keyB := "vendor.com/net-b" + + d1, d2 := deviceID1, deviceID2 + kA, kB := keyA, keyB + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "slice-dual"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + { + Name: device1Name, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &d1}, + multusResourceNameAttr: {StringValue: &kA}, + }, + }, + { + Name: device2Name, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &d2}, + multusResourceNameAttr: {StringValue: &kB}, + }, + }, + }, + }, + } + + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claimName, Namespace: "default"}, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: "req-a", Driver: driverName, Pool: poolName, Device: device1Name}, + {Request: "req-b", Driver: driverName, Pool: poolName, Device: device2Name}, + }, + }, + }, + }, + } + + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-dual", Namespace: "default", UID: k8sTypes.UID("uid-dual")}, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + {Name: claimName, ResourceClaimName: &claimNamePtr}, + }, + }, + } + + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + + Expect(resourceMap).To(HaveKey(keyA)) + Expect(resourceMap[keyA].DeviceIDs).To(Equal([]string{deviceID1})) + Expect(resourceMap).To(HaveKey(keyB)) + Expect(resourceMap[keyB].DeviceIDs).To(Equal([]string{deviceID2})) + }) + }) + + Context("when pod has ExtendedResourceClaimStatus (extended resource feature gate)", func() { + It("should populate resource map using extended resource name as key", func() { + claimName := "ext-resource-dual-port-extended-resources-4sd7g" + device1Name := "0000-08-00-3" + device2Name := "0000-08-02-2" + driverName := "sriovnetwork.k8snetworkplumbingwg.io" + poolName := "c-234-183-40-044" + deviceID1 := "0000:08:00.3" + deviceID2 := "0000:08:02.2" + + deviceID1Value := deviceID1 + deviceID2Value := deviceID2 + rnPort1 := "example.com/sriov-port1" + rnPort2 := "example.com/sriov-port2" + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource-slice", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: device1Name, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &deviceID1Value}, + multusResourceNameAttr: {StringValue: &rnPort1}, + }, + }, + { + Name: device2Name, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &deviceID2Value}, + multusResourceNameAttr: {StringValue: &rnPort2}, + }, + }, + }, + }, + } + + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: "default", + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: "container-0-request-0", + Driver: driverName, + Pool: poolName, + Device: device1Name, + }, + { + Request: "container-0-request-1", + Driver: driverName, + Pool: poolName, + Device: device2Name, + }, + }, + }, + }, + }, + } + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ext-resource-dual-port", + Namespace: "default", + UID: k8sTypes.UID("dc0b90ad-0ca2-4d83-bac9-61e053a86850"), + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: nil, + ExtendedResourceClaimStatus: &v1.PodExtendedResourceClaimStatus{ + ResourceClaimName: claimName, + RequestMappings: []v1.ContainerExtendedResourceRequest{ + { + ContainerName: "test-container", + ResourceName: "example.com/sriov-port1", + RequestName: "container-0-request-0", + }, + { + ContainerName: "test-container", + ResourceName: "example.com/sriov-port2", + RequestName: "container-0-request-1", + }, + }, + }, + }, + } + + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + + Expect(resourceMap).To(HaveKey("example.com/sriov-port1")) + Expect(resourceMap["example.com/sriov-port1"].DeviceIDs).To(Equal([]string{deviceID1})) + Expect(resourceMap).To(HaveKey("example.com/sriov-port2")) + Expect(resourceMap["example.com/sriov-port2"].DeviceIDs).To(Equal([]string{deviceID2})) + }) + + It("should populate resource map with multiple devices when request has count > 1", func() { + claimName := "multus-dual-port-extended-resources-ftknr" + device1Name := "0000-08-00-3" + device2Name := "0000-08-00-4" + device3Name := "0000-08-02-4" + driverName := "sriovnetwork.k8snetworkplumbingwg.io" + poolName := "c-237-169-100-104" + deviceID1 := "0000:08:00.3" + deviceID2 := "0000:08:00.4" + deviceID3 := "0000:08:02.4" + rnP1 := "nvidia.com/sriov-port1" + rnP2 := "nvidia.com/sriov-port2" + + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource-slice", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + {Name: device1Name, Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &deviceID1}, multusResourceNameAttr: {StringValue: &rnP1}, + }}, + {Name: device2Name, Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &deviceID2}, multusResourceNameAttr: {StringValue: &rnP1}, + }}, + {Name: device3Name, Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &deviceID3}, multusResourceNameAttr: {StringValue: &rnP2}, + }}, + }, + }, + } + + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: "default", + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: "container-0-request-0", Driver: driverName, Pool: poolName, Device: device1Name}, + {Request: "container-0-request-0", Driver: driverName, Pool: poolName, Device: device2Name}, + {Request: "container-0-request-1", Driver: driverName, Pool: poolName, Device: device3Name}, + }, + }, + }, + }, + } + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multus-dual-port", + Namespace: "default", + UID: k8sTypes.UID("895bdfd9-a532-4127-97b5-2a0aa5cc91a7"), + }, + Status: v1.PodStatus{ + ExtendedResourceClaimStatus: &v1.PodExtendedResourceClaimStatus{ + ResourceClaimName: claimName, + RequestMappings: []v1.ContainerExtendedResourceRequest{ + {ContainerName: "test-container", ResourceName: "nvidia.com/sriov-port1", RequestName: "container-0-request-0"}, + {ContainerName: "test-container", ResourceName: "nvidia.com/sriov-port2", RequestName: "container-0-request-1"}, + }, + }, + }, + } + + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + + Expect(resourceMap).To(HaveKey("nvidia.com/sriov-port1")) + Expect(resourceMap["nvidia.com/sriov-port1"].DeviceIDs).To(ConsistOf(deviceID1, deviceID2)) + Expect(resourceMap).To(HaveKey("nvidia.com/sriov-port2")) + Expect(resourceMap["nvidia.com/sriov-port2"].DeviceIDs).To(Equal([]string{deviceID3})) + }) + + It("should return an error when device resourceName attribute does not match extended mapping", func() { + claimName := "ext-mismatch-claim" + deviceName := "0000-08-00-3" + driverName := "sriovnetwork.k8snetworkplumbingwg.io" + poolName := "pool-1" + deviceID := "0000:08:00.3" + wrongRN := "wrong.example.com/port" + deviceIDVal := deviceID + + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "slice-mismatch"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + { + Name: deviceName, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &deviceIDVal}, + multusResourceNameAttr: {StringValue: &wrongRN}, + }, + }, + }, + }, + } + + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claimName, Namespace: "default"}, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: "req-0", Driver: driverName, Pool: poolName, Device: deviceName}, + }, + }, + }, + }, + } + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-mismatch", Namespace: "default", UID: k8sTypes.UID("uid-mismatch")}, + Status: v1.PodStatus{ + ExtendedResourceClaimStatus: &v1.PodExtendedResourceClaimStatus{ + ResourceClaimName: claimName, + RequestMappings: []v1.ContainerExtendedResourceRequest{ + {ContainerName: "c", ResourceName: "expected.example.com/port", RequestName: "req-0"}, + }, + }, + }, + } + + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(multusResourceNameAttr)) + Expect(err.Error()).To(ContainSubstring("expected.example.com/port")) + }) + }) + + Context("when resource claim does not exist", func() { + It("should return an error", func() { + claimName := "non-existent-claim" + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: k8sTypes.UID("test-uid"), + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + { + Name: claimName, + ResourceClaimName: &claimNamePtr, + }, + }, + }, + } + + resourceMap := make(map[string]*types.ResourceInfo) + err := draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not found")) + }) + }) + + Context("when resource slice does not exist", func() { + It("should skip the claim and return empty resource map without error", func() { + claimName := "test-claim" + deviceName := "device-1" + driverName := "test-driver.example.com" + poolName := "test-pool" + requestName := "gpu" + + // Create ResourceClaim but no ResourceSlice + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: "default", + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: requestName, + Driver: driverName, + Pool: poolName, + Device: deviceName, + }, + }, + }, + }, + }, + } + + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: k8sTypes.UID("test-uid"), + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + { + Name: claimName, + ResourceClaimName: &claimNamePtr, + }, + }, + }, + } + + // Add only resource claim + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // Execute — no slice means List returns empty; device misses cache → errDeviceNotInAnySlice + // → skipped silently (non-CNI claim graceful handling). No error, empty map. + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + Expect(resourceMap).To(BeEmpty()) + }) + }) + + Context("when device does not have deviceID attribute", func() { + It("should warn and skip the claim without failing", func() { + claimName := "test-claim" + deviceName := "device-1" + driverName := "test-driver.example.com" + poolName := "test-pool" + requestName := "gpu" + + // Create ResourceSlice with device but NO deviceID attribute + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource-slice", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: deviceName, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + // Missing multusDeviceIDAttr + "some-other-attribute": { + StringValue: func() *string { s := "value"; return &s }(), + }, + }, + }, + }, + }, + } + + // Create ResourceClaim + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: "default", + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: requestName, + Driver: driverName, + Pool: poolName, + Device: deviceName, + }, + }, + }, + }, + }, + } + + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: k8sTypes.UID("test-uid"), + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + { + Name: claimName, + ResourceClaimName: &claimNamePtr, + }, + }, + }, + } + + // Add objects + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // Execute + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + Expect(resourceMap).To(BeEmpty()) + }) + }) + + Context("when device has deviceID but missing resourceName attribute", func() { + It("should warn and skip without failing when nothing maps to a resource name", func() { + claimName := "test-claim" + deviceName := "device-1" + driverName := "test-driver.example.com" + poolName := "test-pool" + requestName := "gpu" + deviceID := "pci:0000:00:01.0" + deviceIDValue := deviceID + + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "test-resource-slice"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + { + Name: deviceName, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &deviceIDValue}, + }, + }, + }, + }, + } + + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claimName, Namespace: "default"}, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: requestName, Driver: driverName, Pool: poolName, Device: deviceName}, + }, + }, + }, + }, + } + + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "test-pod", Namespace: "default", UID: k8sTypes.UID("test-uid")}, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + {Name: claimName, ResourceClaimName: &claimNamePtr}, + }, + }, + } + + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + Expect(resourceMap).To(BeEmpty()) + }) + }) + + Context("when device name in allocation does not match any device in slice", func() { + It("should warn and skip the claim without failing", func() { + claimName := "test-claim" + deviceName := "device-1" + wrongDeviceName := "wrong-device" + driverName := "test-driver.example.com" + poolName := "test-pool" + requestName := "gpu" + deviceID := "pci:0000:00:01.0" + + // Create ResourceSlice with device + deviceIDValue := deviceID + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource-slice", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: deviceName, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: { + StringValue: &deviceIDValue, + }, + }, + }, + }, + }, + } + + // Create ResourceClaim with WRONG device name + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: "default", + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: requestName, + Driver: driverName, + Pool: poolName, + Device: wrongDeviceName, + }, + }, + }, + }, + }, + } + + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: k8sTypes.UID("test-uid"), + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + { + Name: claimName, + ResourceClaimName: &claimNamePtr, + }, + }, + }, + } + + // Add objects + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // Execute + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + Expect(resourceMap).To(BeEmpty()) + }) + + It("should preserve existing kubelet map entries when the claim maps nothing", func() { + claimName := "test-claim" + deviceName := "device-1" + wrongDeviceName := "wrong-device" + driverName := "test-driver.example.com" + poolName := "test-pool" + requestName := "gpu" + deviceID := "pci:0000:00:01.0" + legacyKey := "example.com/legacy-vf" + legacyPCI := "0000:8d:00.4" + + deviceIDValue := deviceID + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "test-resource-slice"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + { + Name: deviceName, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &deviceIDValue}, + }, + }, + }, + }, + } + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claimName, Namespace: "default"}, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: requestName, Driver: driverName, Pool: poolName, Device: wrongDeviceName}, + }, + }, + }, + }, + } + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "hybrid-pod", Namespace: "default", UID: k8sTypes.UID("uid-hybrid")}, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + {Name: claimName, ResourceClaimName: &claimNamePtr}, + }, + }, + } + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + resourceMap := map[string]*types.ResourceInfo{ + legacyKey: {DeviceIDs: []string{legacyPCI}}, + } + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + Expect(resourceMap[legacyKey].DeviceIDs).To(Equal([]string{legacyPCI})) + }) + + It("should succeed when one allocation result is missing from slices but another resolves", func() { + claimName := "multi-claim" + driverSRIOV := "sriovnetwork.k8snetworkplumbingwg.io" + driverGPU := "gpu.nvidia.com" + poolName := "orch-dev-a100-002" + deviceVF := "0000-8d-00-4" + deviceGPU := "gpu-4" + deviceIDVF := "pci:0000:8d:00.4" + mapKey := "nvidia.com/port2" + deviceIDVFVal := deviceIDVF + mapKeyVal := mapKey + + // SR-IOV slice has the VF; empty GPU slice exists so List() finds gpu.nvidia.com/pool (real clusters + // always publish a slice per driver/pool). Allocation still references gpu-4, which is not listed here. + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "sriov-slice"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverSRIOV, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + { + Name: deviceVF, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &deviceIDVFVal}, + multusResourceNameAttr: {StringValue: &mapKeyVal}, + }, + }, + }, + }, + } + gpuSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "gpu-slice"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverGPU, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{}, + }, + } + + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claimName, Namespace: "default"}, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: "vf", Driver: driverSRIOV, Pool: poolName, Device: deviceVF}, + {Request: "gpu", Driver: driverGPU, Pool: poolName, Device: deviceGPU}, + }, + }, + }, + }, + } + + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "mixed-pod", Namespace: "default", UID: k8sTypes.UID("uid-mixed")}, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + {Name: claimName, ResourceClaimName: &claimNamePtr}, + }, + }, + } + + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), gpuSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + Expect(resourceMap[mapKey].DeviceIDs).To(Equal([]string{deviceIDVF})) + }) + }) + + Context("when caching is working correctly", func() { + It("should cache resource claims and slices", func() { + claimName := "test-claim" + deviceName := "device-1" + driverName := "test-driver.example.com" + poolName := "test-pool" + requestName := "gpu" + deviceID := "pci:0000:00:01.0" + mapKey := "cache-test.example.com/res" + + // Create ResourceSlice with device + deviceIDValue := deviceID + mapKeyValue := mapKey + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource-slice", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: deviceName, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: { + StringValue: &deviceIDValue, + }, + multusResourceNameAttr: {StringValue: &mapKeyValue}, + }, + }, + }, + }, + } + + // Create ResourceClaim + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: "default", + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: requestName, + Driver: driverName, + Pool: poolName, + Device: deviceName, + }, + }, + }, + }, + }, + } + + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: k8sTypes.UID("test-uid"), + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + { + Name: claimName, + ResourceClaimName: &claimNamePtr, + }, + }, + }, + } + + // Add objects to fake client + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // First call - should populate cache + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + + expectedKey := mapKey + Expect(resourceMap).To(HaveKey(expectedKey)) + Expect(resourceMap[expectedKey].DeviceIDs).To(Equal([]string{deviceID})) + + // Delete the objects from the API server + err = fakeClient.ResourceV1().ResourceClaims("default").Delete(context.TODO(), claimName, metav1.DeleteOptions{}) + Expect(err).NotTo(HaveOccurred()) + err = fakeClient.ResourceV1().ResourceSlices().Delete(context.TODO(), resourceSlice.Name, metav1.DeleteOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // Second call - should use cache and succeed even though objects are deleted + resourceMap2 := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap2) + Expect(err).NotTo(HaveOccurred()) + Expect(resourceMap2).To(HaveKey(expectedKey)) + Expect(resourceMap2[expectedKey].DeviceIDs).To(Equal([]string{deviceID})) + }) + }) + + Context("when two claims share the same driver", func() { + It("should populate the driver cache only once and resolve both devices", func() { + driverName := "sriovnetwork.k8snetworkplumbingwg.io" + poolName := "test-pool" + device1Name := "dev-a" + device2Name := "dev-b" + deviceID1 := "pci:0000:01:00.0" + deviceID2 := "pci:0000:02:00.0" + mapKey1 := "sriov.example.com/net-a" + mapKey2 := "sriov.example.com/net-b" + claim1Name := "claim-a" + claim2Name := "claim-b" + + dv1, dv2, mk1, mk2 := deviceID1, deviceID2, mapKey1, mapKey2 + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "sriov-slice"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + {Name: device1Name, Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &dv1}, multusResourceNameAttr: {StringValue: &mk1}, + }}, + {Name: device2Name, Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &dv2}, multusResourceNameAttr: {StringValue: &mk2}, + }}, + }, + }, + } + + c1ptr, c2ptr := claim1Name, claim2Name + resourceClaim1 := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claim1Name, Namespace: "default"}, + Status: resourcev1api.ResourceClaimStatus{Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: "req-a", Driver: driverName, Pool: poolName, Device: device1Name}, + }}, + }}, + } + resourceClaim2 := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claim2Name, Namespace: "default"}, + Status: resourcev1api.ResourceClaimStatus{Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: "req-b", Driver: driverName, Pool: poolName, Device: device2Name}, + }}, + }}, + } + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "two-claim-pod", Namespace: "default", UID: k8sTypes.UID("uid-two")}, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + {Name: claim1Name, ResourceClaimName: &c1ptr}, + {Name: claim2Name, ResourceClaimName: &c2ptr}, + }, + }, + } + + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim1, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim2, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + + Expect(resourceMap).To(HaveKey(mapKey1)) + Expect(resourceMap[mapKey1].DeviceIDs).To(Equal([]string{deviceID1})) + Expect(resourceMap).To(HaveKey(mapKey2)) + Expect(resourceMap[mapKey2].DeviceIDs).To(Equal([]string{deviceID2})) + + // Verify the driver cache was populated after the first call: delete the slice from + // the API server and call again — if cache is working, it must still resolve correctly. + err = fakeClient.ResourceV1().ResourceSlices().Delete(context.TODO(), resourceSlice.Name, metav1.DeleteOptions{}) + Expect(err).NotTo(HaveOccurred()) + + resourceMap2 := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap2) + Expect(err).NotTo(HaveOccurred()) + Expect(resourceMap2[mapKey1].DeviceIDs).To(Equal([]string{deviceID1})) + Expect(resourceMap2[mapKey2].DeviceIDs).To(Equal([]string{deviceID2})) + }) + }) + + Context("when ResourceClaim names collide across namespaces", func() { + It("should cache claims separately per namespace", func() { + claimName := "same-claim-name" + driverName := "test-driver.example.com" + poolName := "test-pool" + requestName := "gpu" + mapKey := "cross-ns.example.com/res" + nsA, nsB := "team-a", "team-b" + deviceA, deviceB := "dev-a", "dev-b" + pciA, pciB := "pci:0000:01:00.0", "pci:0000:02:00.0" + + mapKeyVal := mapKey + pciAVal, pciBVal := pciA, pciB + + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "shared-slice"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + { + Name: deviceA, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &pciAVal}, + multusResourceNameAttr: {StringValue: &mapKeyVal}, + }, + }, + { + Name: deviceB, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &pciBVal}, + multusResourceNameAttr: {StringValue: &mapKeyVal}, + }, + }, + }, + }, + } + + claimA := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claimName, Namespace: nsA}, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: requestName, Driver: driverName, Pool: poolName, Device: deviceA}, + }, + }, + }, + }, + } + claimB := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claimName, Namespace: nsB}, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: requestName, Driver: driverName, Pool: poolName, Device: deviceB}, + }, + }, + }, + }, + } + + _, err := fakeClient.ResourceV1().ResourceClaims(nsA).Create(context.TODO(), claimA, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceClaims(nsB).Create(context.TODO(), claimB, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + claimPtr := claimName + podA := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-a", Namespace: nsA, UID: k8sTypes.UID("uid-a")}, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + {Name: claimName, ResourceClaimName: &claimPtr}, + }, + }, + } + podB := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-b", Namespace: nsB, UID: k8sTypes.UID("uid-b")}, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + {Name: claimName, ResourceClaimName: &claimPtr}, + }, + }, + } + + m1 := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), podA, m1) + Expect(err).NotTo(HaveOccurred()) + Expect(m1[mapKey].DeviceIDs).To(Equal([]string{pciA})) + + m2 := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), podB, m2) + Expect(err).NotTo(HaveOccurred()) + Expect(m2[mapKey].DeviceIDs).To(Equal([]string{pciB})) + }) + }) + + Context("when pod has multiple different claims", func() { + It("should populate resource map with all claims sequentially", func() { + // Note: Due to fake client limitations with field selectors, + // we test each claim separately to avoid conflicts + + claim1Name := "claim-1" + device1Name := "device-1" + driver1Name := "driver1.example.com" + pool1Name := "pool-1" + request1Name := "gpu" + deviceID1 := "pci:0000:00:01.0" + mapKey1 := "driver1.example.com/gpu-net" + + // First claim setup + deviceID1Value := deviceID1 + mapKey1Val := mapKey1 + resourceSlice1 := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource-slice-1", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driver1Name, + Pool: resourcev1api.ResourcePool{ + Name: pool1Name, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: device1Name, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: { + StringValue: &deviceID1Value, + }, + multusResourceNameAttr: {StringValue: &mapKey1Val}, + }, + }, + }, + }, + } + + resourceClaim1 := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claim1Name, + Namespace: "default", + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: request1Name, + Driver: driver1Name, + Pool: pool1Name, + Device: device1Name, + }, + }, + }, + }, + }, + } + + claim1NamePtr := claim1Name + pod1 := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-1", + Namespace: "default", + UID: k8sTypes.UID("test-uid-1"), + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + { + Name: claim1Name, + ResourceClaimName: &claim1NamePtr, + }, + }, + }, + } + + // Add first claim objects + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim1, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice1, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // Test first claim + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod1, resourceMap) + Expect(err).NotTo(HaveOccurred()) + + Expect(resourceMap).To(HaveKey(mapKey1)) + Expect(resourceMap[mapKey1].DeviceIDs).To(Equal([]string{deviceID1})) + + // Now test second claim with a fresh client to avoid field selector issues + claim2Name := "claim-2" + device2Name := "device-2" + driver2Name := "driver2.example.com" + pool2Name := "pool-2" + request2Name := "nic" + deviceID2 := "pci:0000:00:02.0" + mapKey2 := "driver2.example.com/nic-net" + + fakeClient2 := fake.NewClientset() + draClient2 := NewClient(fakeClient2.ResourceV1()) + + deviceID2Value := deviceID2 + mapKey2Val := mapKey2 + resourceSlice2 := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource-slice-2", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driver2Name, + Pool: resourcev1api.ResourcePool{ + Name: pool2Name, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: device2Name, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: { + StringValue: &deviceID2Value, + }, + multusResourceNameAttr: {StringValue: &mapKey2Val}, + }, + }, + }, + }, + } + + resourceClaim2 := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claim2Name, + Namespace: "default", + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: request2Name, + Driver: driver2Name, + Pool: pool2Name, + Device: device2Name, + }, + }, + }, + }, + }, + } + + claim2NamePtr := claim2Name + pod2 := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-2", + Namespace: "default", + UID: k8sTypes.UID("test-uid-2"), + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + { + Name: claim2Name, + ResourceClaimName: &claim2NamePtr, + }, + }, + }, + } + + // Add second claim objects + _, err = fakeClient2.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim2, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient2.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice2, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // Test second claim + resourceMap2 := make(map[string]*types.ResourceInfo) + err = draClient2.GetPodResourceMap(context.TODO(), pod2, resourceMap2) + Expect(err).NotTo(HaveOccurred()) + + Expect(resourceMap2).To(HaveKey(mapKey2)) + Expect(resourceMap2[mapKey2].DeviceIDs).To(Equal([]string{deviceID2})) + }) + }) + + Context("when resource map already has an entry for the claim/request", func() { + It("should append device IDs to existing entry", func() { + claimName := "test-claim" + deviceName := "device-1" + driverName := "test-driver.example.com" + poolName := "test-pool" + requestName := "gpu" + deviceID := "pci:0000:00:01.0" + existingDeviceID := "pci:0000:00:00.0" + mapKey := "append-test.example.com/res" + + // Create ResourceSlice with device + deviceIDValue := deviceID + mapKeyValue := mapKey + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource-slice", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: deviceName, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: { + StringValue: &deviceIDValue, + }, + multusResourceNameAttr: {StringValue: &mapKeyValue}, + }, + }, + }, + }, + } + + // Create ResourceClaim + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: "default", + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: requestName, + Driver: driverName, + Pool: poolName, + Device: deviceName, + }, + }, + }, + }, + }, + } + + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: k8sTypes.UID("test-uid"), + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + { + Name: claimName, + ResourceClaimName: &claimNamePtr, + }, + }, + }, + } + + // Add objects + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // Pre-populate resourceMap with existing entry + resourceMap := make(map[string]*types.ResourceInfo) + expectedKey := mapKey + resourceMap[expectedKey] = &types.ResourceInfo{ + DeviceIDs: []string{existingDeviceID}, + } + + // Execute + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + + // Verify device ID was appended + Expect(resourceMap).To(HaveKey(expectedKey)) + Expect(resourceMap[expectedKey].DeviceIDs).To(Equal([]string{existingDeviceID, deviceID})) + }) + }) + + Context("when multiple resource slices exist for the same driver/pool", func() { + It("should find the allocated device by searching all slices", func() { + claimName := "test-claim" + deviceName := "device-1" + driverName := "test-driver.example.com" + poolName := "test-pool" + requestName := "gpu" + deviceID := "pci:0000:00:01.0" + mapKey := "multi-slice.example.com/res" + + // DRA allows multiple ResourceSlices per driver/pool (e.g. per NUMA). Allocation references device-1 + // which lives only in the first slice; second slice holds another device. + deviceIDValue := deviceID + mapKeyValue := mapKey + resourceSlice1 := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource-slice-1", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: deviceName, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: { + StringValue: &deviceIDValue, + }, + multusResourceNameAttr: {StringValue: &mapKeyValue}, + }, + }, + }, + }, + } + + resourceSlice2 := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource-slice-2", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: "device-2", + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: { + StringValue: &deviceIDValue, + }, + multusResourceNameAttr: {StringValue: &mapKeyValue}, + }, + }, + }, + }, + } + + // Create ResourceClaim + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: "default", + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: requestName, + Driver: driverName, + Pool: poolName, + Device: deviceName, + }, + }, + }, + }, + }, + } + + claimNamePtr := claimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: k8sTypes.UID("test-uid"), + }, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + { + Name: claimName, + ResourceClaimName: &claimNamePtr, + }, + }, + }, + } + + _, err := fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + var objects []runtime.Object + objects = append(objects, resourceSlice1, resourceSlice2) + fakeClient2 := fake.NewClientset(objects...) + draClient2 := NewClient(fakeClient2.ResourceV1()) + + _, err = fakeClient2.ResourceV1().ResourceClaims("default").Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient2.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + Expect(resourceMap).To(HaveKey(mapKey)) + Expect(resourceMap[mapKey].DeviceIDs).To(Equal([]string{deviceID})) + }) + }) + + Context("when pod has both regular ResourceClaimStatuses and ExtendedResourceClaimStatus", func() { + It("should populate resource map from both paths independently", func() { + driverName := "sriovnetwork.k8snetworkplumbingwg.io" + poolName := "test-pool" + + // Regular claim: one VF mapped via ResourceClaimStatuses + regularClaimName := "regular-claim" + regularDeviceName := "vf-0" + regularDeviceID := "pci:0000:01:00.0" + regularMapKey := "sriov.example.com/vf-net" + + // Extended claim: two ports mapped via ExtendedResourceClaimStatus + extClaimName := "extended-claim" + extDevice1Name := "vf-1" + extDevice2Name := "vf-2" + extDeviceID1 := "pci:0000:02:00.0" + extDeviceID2 := "pci:0000:03:00.0" + extMapKey1 := "sriov.example.com/port-a" + extMapKey2 := "sriov.example.com/port-b" + + rdID, rmk := regularDeviceID, regularMapKey + ed1, ed2 := extDeviceID1, extDeviceID2 + emk1, emk2 := extMapKey1, extMapKey2 + + // One ResourceSlice covers all devices for the driver + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "sriov-slice"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + {Name: regularDeviceName, Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &rdID}, multusResourceNameAttr: {StringValue: &rmk}, + }}, + {Name: extDevice1Name, Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &ed1}, multusResourceNameAttr: {StringValue: &emk1}, + }}, + {Name: extDevice2Name, Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + multusDeviceIDAttr: {StringValue: &ed2}, multusResourceNameAttr: {StringValue: &emk2}, + }}, + }, + }, + } + + regularClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: regularClaimName, Namespace: "default"}, + Status: resourcev1api.ResourceClaimStatus{Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: "vf", Driver: driverName, Pool: poolName, Device: regularDeviceName}, + }}, + }}, + } + + extClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: extClaimName, Namespace: "default"}, + Status: resourcev1api.ResourceClaimStatus{Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: "req-port-a", Driver: driverName, Pool: poolName, Device: extDevice1Name}, + {Request: "req-port-b", Driver: driverName, Pool: poolName, Device: extDevice2Name}, + }}, + }}, + } + + regularClaimPtr := regularClaimName + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "combo-pod", Namespace: "default", UID: k8sTypes.UID("uid-combo")}, + Status: v1.PodStatus{ + ResourceClaimStatuses: []v1.PodResourceClaimStatus{ + {Name: regularClaimName, ResourceClaimName: ®ularClaimPtr}, + }, + ExtendedResourceClaimStatus: &v1.PodExtendedResourceClaimStatus{ + ResourceClaimName: extClaimName, + RequestMappings: []v1.ContainerExtendedResourceRequest{ + {ContainerName: "c", ResourceName: extMapKey1, RequestName: "req-port-a"}, + {ContainerName: "c", ResourceName: extMapKey2, RequestName: "req-port-b"}, + }, + }, + }, + } + + _, err := fakeClient.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), regularClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = fakeClient.ResourceV1().ResourceClaims("default").Create(context.TODO(), extClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + resourceMap := make(map[string]*types.ResourceInfo) + err = draClient.GetPodResourceMap(context.TODO(), pod, resourceMap) + Expect(err).NotTo(HaveOccurred()) + + // Regular claim path + Expect(resourceMap).To(HaveKey(regularMapKey)) + Expect(resourceMap[regularMapKey].DeviceIDs).To(Equal([]string{regularDeviceID})) + + // Extended resource claim path + Expect(resourceMap).To(HaveKey(extMapKey1)) + Expect(resourceMap[extMapKey1].DeviceIDs).To(Equal([]string{extDeviceID1})) + Expect(resourceMap).To(HaveKey(extMapKey2)) + Expect(resourceMap[extMapKey2].DeviceIDs).To(Equal([]string{extDeviceID2})) + + Expect(resourceMap).To(HaveLen(3)) + }) + }) + }) +}) diff --git a/pkg/k8sclient/k8sclient.go b/pkg/k8sclient/k8sclient.go index b809a5376..df8c2baf8 100644 --- a/pkg/k8sclient/k8sclient.go +++ b/pkg/k8sclient/k8sclient.go @@ -42,6 +42,7 @@ import ( netclient "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned" netlister "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/listers/k8s.cni.cncf.io/v1" netutils "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/utils" + "gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/draclient" "gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/kubeletclient" "gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/logging" "gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/types" @@ -53,6 +54,10 @@ const ( networkAttachmentAnnot = "k8s.v1.cni.cncf.io/networks" ) +// getResourceClientFunc returns kubelet / device-plugin resource info for a pod. +// It defaults to kubeletclient.GetResourceClient; unit tests replace it to avoid a real kubelet checkpoint/socket. +var getResourceClientFunc = kubeletclient.GetResourceClient + // NoK8sNetworkError indicates error, no network in kubernetes type NoK8sNetworkError struct { message string @@ -309,7 +314,7 @@ func getKubernetesDelegate(client *ClientInfo, net *types.NetworkSelectionElemen logging.Debugf("getKubernetesDelegate: found resourceName annotation : %s", resourceName) if resourceMap == nil { - ck, err := kubeletclient.GetResourceClient("") + ck, err := getResourceClientFunc("") if err != nil { return nil, resourceMap, logging.Errorf("getKubernetesDelegate: failed to get a ResourceClient instance: %v", err) } @@ -317,6 +322,13 @@ func getKubernetesDelegate(client *ClientInfo, net *types.NetworkSelectionElemen if err != nil { return nil, resourceMap, logging.Errorf("getKubernetesDelegate: failed to get resourceMap from ResourceClient: %v", err) } + + dc := draclient.NewClient(client.Client.ResourceV1()) + err = dc.GetPodResourceMap(context.TODO(), pod, resourceMap) + if err != nil { + return nil, resourceMap, logging.Errorf("getKubernetesDelegate: failed to get resourceMap from DRA client: %v", err) + } + logging.Debugf("getKubernetesDelegate: resourceMap instance: %+v", resourceMap) } diff --git a/pkg/k8sclient/k8sclient_test.go b/pkg/k8sclient/k8sclient_test.go index 7e9a9332c..eb2621c53 100644 --- a/pkg/k8sclient/k8sclient_test.go +++ b/pkg/k8sclient/k8sclient_test.go @@ -18,6 +18,7 @@ package k8sclient // disable dot-imports only for testing //revive:disable:dot-imports import ( + "context" "fmt" "os" "path/filepath" @@ -33,6 +34,9 @@ import ( netfake "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned/fake" netutils "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/utils" + v1 "k8s.io/api/core/v1" + resourcev1api "k8s.io/api/resource/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" . "github.com/onsi/ginkgo/v2" @@ -52,6 +56,28 @@ func NewFakeClientInfo() *ClientInfo { } } +// fakeEmptyResourceClient implements types.ResourceClient with no device allocations (stubs kubelet in tests). +type fakeEmptyResourceClient struct{} + +func (*fakeEmptyResourceClient) GetPodResourceMap(*v1.Pod) (map[string]*types.ResourceInfo, error) { + return make(map[string]*types.ResourceInfo), nil +} + +// fakeResourceClient implements types.ResourceClient with a pre-seeded device-plugin resource map, +// simulating what the kubelet returns for device-plugin (non-DRA) allocations. +type fakeResourceClient struct { + resourceMap map[string]*types.ResourceInfo +} + +func (f *fakeResourceClient) GetPodResourceMap(*v1.Pod) (map[string]*types.ResourceInfo, error) { + result := make(map[string]*types.ResourceInfo) + for k, v := range f.resourceMap { + cp := &types.ResourceInfo{DeviceIDs: append([]string(nil), v.DeviceIDs...)} + result[k] = cp + } + return result, nil +} + var _ = Describe("k8sclient operations", func() { var tmpDir string var err error @@ -1553,4 +1579,697 @@ users: Expect(err).NotTo(HaveOccurred()) }) }) + + Describe("DRA (Dynamic Resource Allocation) integration", func() { + var tmpDir string + var err error + + BeforeEach(func() { + tmpDir, err = os.MkdirTemp("", "multus_dra_test") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + err := os.RemoveAll(tmpDir) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("when pod has a DRA claim and the NAD carries a resourceName annotation", func() { + // Override getResourceClientFunc so the kubelet stub returns an empty map, + // letting us isolate the DRA branch in getKubernetesDelegate. + var origGetResourceClient func(string) (types.ResourceClient, error) + BeforeEach(func() { + origGetResourceClient = getResourceClientFunc + getResourceClientFunc = func(string) (types.ResourceClient, error) { + return &fakeEmptyResourceClient{}, nil + } + }) + AfterEach(func() { getResourceClientFunc = origGetResourceClient }) + + It("should populate resourceMap from the DRA ResourceSlice when resourceMap is nil", func() { + const fakePodName = "dra-test-pod" + const fakeNamespace = "default" + const sriovResName = "sriovnetwork.k8snetworkplumbingwg.io/sriov_vf" + + netAttachDef := `{"name":"sriov-net","type":"sriov","cniVersion":"0.3.1"}` + claimName := "sriov-claim" + claimNamePtr := claimName + driverName := "sriovnetwork.k8snetworkplumbingwg.io" + poolName := "sriov-pool" + deviceName := "vf-1" + deviceID := "pci:0000:00:01.0" + + fakePod := testutils.NewFakePod(fakePodName, "sriov-net", "") + fakePod.Namespace = fakeNamespace + fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{ + {Name: claimName, ResourceClaimName: &claimNamePtr}, + } + + clientInfo := NewFakeClientInfo() + _, err = clientInfo.AddPod(fakePod) + Expect(err).NotTo(HaveOccurred()) + + // NAD carries the resourceName annotation — this triggers the DRA lookup path. + nad := testutils.NewFakeNetAttachDef(fakeNamespace, "sriov-net", netAttachDef) + nad.Annotations = map[string]string{resourceNameAnnot: sriovResName} + _, err = clientInfo.AddNetAttachDef(nad) + Expect(err).NotTo(HaveOccurred()) + + deviceIDVal := deviceID + resNameVal := sriovResName + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "sriov-slice"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + {Name: deviceName, Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + "k8s.cni.cncf.io/deviceID": {StringValue: &deviceIDVal}, + "k8s.cni.cncf.io/resourceName": {StringValue: &resNameVal}, + }}, + }, + }, + } + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claimName, Namespace: fakeNamespace}, + Status: resourcev1api.ResourceClaimStatus{Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: "sriov", Driver: driverName, Pool: poolName, Device: deviceName}, + }}, + }}, + } + + _, err = clientInfo.Client.ResourceV1().ResourceClaims(fakeNamespace).Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = clientInfo.Client.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + net := &types.NetworkSelectionElement{Name: "sriov-net", Namespace: fakeNamespace} + // Pass nil so getKubernetesDelegate enters the DRA lookup branch. + delegate, resourceMap, err := getKubernetesDelegate(clientInfo, net, tmpDir, fakePod, nil) + Expect(err).NotTo(HaveOccurred()) + Expect(delegate).NotTo(BeNil()) + + // The DRA device ID must be in the resource map under the NAD resourceName key. + Expect(resourceMap).To(HaveKey(sriovResName)) + Expect(resourceMap[sriovResName].DeviceIDs).To(ContainElement(deviceID)) + }) + }) + + Context("when GetNetworkDelegates is called with DRA-enabled pod", func() { + It("should successfully retrieve delegates with DRA resources", func() { + const fakePodName string = "dra-network-pod" + const fakeNamespace string = "default" + + // Create network attachment definition + netAttachDef := `{ + "name": "dra-network", + "type": "bridge", + "cniVersion": "0.3.1" + }` + + // Create fake pod with DRA resource claims + claimName := "network-claim" + claimNamePtr := &claimName + fakePod := testutils.NewFakePod(fakePodName, "dra-network", "") + fakePod.Namespace = fakeNamespace + fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{ + { + ResourceClaimName: claimNamePtr, + }, + } + + // Setup client + clientInfo := NewFakeClientInfo() + _, err = clientInfo.AddPod(fakePod) + Expect(err).NotTo(HaveOccurred()) + + // Create network attachment definition with resource name annotation + nad := testutils.NewFakeNetAttachDef(fakeNamespace, "dra-network", netAttachDef) + nad.Annotations = map[string]string{ + resourceNameAnnot: "dra-network-claim/gpu", + } + _, err = clientInfo.AddNetAttachDef(nad) + Expect(err).NotTo(HaveOccurred()) + + // Create DRA resources + deviceName := "nic-1" + driverName := "network.example.com" + poolName := "network-pool" + requestName := "gpu" + deviceID := "pci:0000:00:02.0" + // NAD k8s.v1.cni.cncf.io/resourceName must match device attribute k8s.cni.cncf.io/resourceName + nadResourceKey := "dra-network-claim/gpu" + + deviceIDValue := deviceID + nadResourceKeyVal := nadResourceKey + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "network-resource-slice", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: deviceName, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + "k8s.cni.cncf.io/deviceID": { + StringValue: &deviceIDValue, + }, + "k8s.cni.cncf.io/resourceName": { + StringValue: &nadResourceKeyVal, + }, + }, + }, + }, + }, + } + + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: fakeNamespace, + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: requestName, + Driver: driverName, + Pool: poolName, + Device: deviceName, + }, + }, + }, + }, + }, + } + + // Add DRA resources to fake client + _, err = clientInfo.Client.ResourceV1().ResourceClaims(fakeNamespace).Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = clientInfo.Client.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // Get pod network + networks, err := GetPodNetwork(fakePod) + Expect(err).NotTo(HaveOccurred()) + Expect(networks).NotTo(BeEmpty()) + + // Create NetConf + conf := &types.NetConf{ + ConfDir: tmpDir, + } + + // Get network delegates + resourceMap := make(map[string]*types.ResourceInfo) + delegates, err := GetNetworkDelegates(clientInfo, fakePod, networks, conf, resourceMap) + Expect(err).NotTo(HaveOccurred()) + Expect(delegates).NotTo(BeEmpty()) + Expect(delegates[0]).NotTo(BeNil()) + }) + }) + + Context("when DRA resources are used in TryLoadPodDelegates", func() { + It("should successfully load delegates with DRA resources", func() { + const fakePodName string = "dra-delegate-pod" + const fakeNamespace string = "default" + + // Create network attachment definition + netAttachDef := `{ + "name": "dra-delegate-network", + "type": "macvlan", + "cniVersion": "0.3.1" + }` + + // Create fake pod with DRA resource claims + claimName := "delegate-claim" + claimNamePtr := &claimName + fakePod := testutils.NewFakePod(fakePodName, "dra-delegate-network", "") + fakePod.Namespace = fakeNamespace + fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{ + { + ResourceClaimName: claimNamePtr, + }, + } + + // Setup client + clientInfo := NewFakeClientInfo() + _, err = clientInfo.AddPod(fakePod) + Expect(err).NotTo(HaveOccurred()) + + // Create network attachment definition + nad := testutils.NewFakeNetAttachDef(fakeNamespace, "dra-delegate-network", netAttachDef) + _, err = clientInfo.AddNetAttachDef(nad) + Expect(err).NotTo(HaveOccurred()) + + // Create DRA resources + deviceName := "macvlan-device" + driverName := "macvlan.example.com" + poolName := "macvlan-pool" + requestName := "nic" + deviceID := "pci:0000:00:03.0" + + deviceIDValue := deviceID + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "macvlan-resource-slice", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: deviceName, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + "k8s.cni.cncf.io/deviceID": { + StringValue: &deviceIDValue, + }, + }, + }, + }, + }, + } + + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: fakeNamespace, + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: requestName, + Driver: driverName, + Pool: poolName, + Device: deviceName, + }, + }, + }, + }, + }, + } + + // Add DRA resources to fake client + _, err = clientInfo.Client.ResourceV1().ResourceClaims(fakeNamespace).Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = clientInfo.Client.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // Create NetConf with a default delegate + confStr := `{ + "name": "test-network", + "type": "multus", + "delegates": [{ + "name": "default-network", + "type": "bridge" + }], + "confDir": "` + tmpDir + `" + }` + + conf, err := types.LoadNetConf([]byte(confStr)) + Expect(err).NotTo(HaveOccurred()) + + // Try loading pod delegates + resourceMap := make(map[string]*types.ResourceInfo) + count, updatedClient, err := TryLoadPodDelegates(fakePod, conf, clientInfo, resourceMap) + Expect(err).NotTo(HaveOccurred()) + Expect(updatedClient).NotTo(BeNil()) + Expect(count).To(BeNumerically(">", 0)) + }) + }) + + Context("when pod has no DRA resources", func() { + It("should handle pods without DRA resources gracefully", func() { + const fakePodName string = "no-dra-pod" + const fakeNamespace string = "default" + + // Create network attachment definition + netAttachDef := `{ + "name": "simple-network", + "type": "bridge", + "cniVersion": "0.3.1" + }` + + // Create fake pod WITHOUT DRA resource claims + fakePod := testutils.NewFakePod(fakePodName, "simple-network", "") + fakePod.Namespace = fakeNamespace + fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{} // Empty + + // Setup client + clientInfo := NewFakeClientInfo() + _, err = clientInfo.AddPod(fakePod) + Expect(err).NotTo(HaveOccurred()) + + // Create network attachment definition + nad := testutils.NewFakeNetAttachDef(fakeNamespace, "simple-network", netAttachDef) + _, err = clientInfo.AddNetAttachDef(nad) + Expect(err).NotTo(HaveOccurred()) + + // Get the network selection element + net := &types.NetworkSelectionElement{ + Name: "simple-network", + Namespace: fakeNamespace, + } + + // Call getKubernetesDelegate - should work without DRA + delegate, resourceMap, err := getKubernetesDelegate(clientInfo, net, tmpDir, fakePod, nil) + Expect(err).NotTo(HaveOccurred()) + Expect(delegate).NotTo(BeNil()) + // ResourceMap should be empty (no DRA resources) + Expect(len(resourceMap)).To(Equal(0)) + }) + }) + + Context("when DRA client fails to get resources", func() { + var origGetResourceClient func(string) (types.ResourceClient, error) + + BeforeEach(func() { + origGetResourceClient = getResourceClientFunc + // Stub kubelet so we get past device-plugin checkpoint/socket and exercise draclient. + getResourceClientFunc = func(string) (types.ResourceClient, error) { + return &fakeEmptyResourceClient{}, nil + } + }) + + AfterEach(func() { + getResourceClientFunc = origGetResourceClient + }) + + It("should return an error from getKubernetesDelegate", func() { + const fakePodName string = "dra-fail-pod" + const fakeNamespace string = "default" + const resourceName = "example.com/gpu" + + // Create network attachment definition + netAttachDef := `{ + "name": "fail-network", + "type": "bridge", + "cniVersion": "0.3.1" + }` + + // Create fake pod with DRA resource claims that don't exist in the API + claimName := "non-existent-claim" + claimNamePtr := &claimName + fakePod := testutils.NewFakePod(fakePodName, "fail-network", "") + fakePod.Namespace = fakeNamespace + fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{ + { + Name: claimName, + ResourceClaimName: claimNamePtr, + }, + } + + // Setup client + clientInfo := NewFakeClientInfo() + _, err = clientInfo.AddPod(fakePod) + Expect(err).NotTo(HaveOccurred()) + + // NAD must have resourceName annotation so getKubernetesDelegate runs the DRA client path + nad := testutils.NewFakeNetAttachDef(fakeNamespace, "fail-network", netAttachDef) + nad.Annotations = map[string]string{ + resourceNameAnnot: resourceName, + } + _, err = clientInfo.AddNetAttachDef(nad) + Expect(err).NotTo(HaveOccurred()) + + // Do NOT create the ResourceClaim — draclient.GetPodResourceMap fails on API Get. + + net := &types.NetworkSelectionElement{ + Name: "fail-network", + Namespace: fakeNamespace, + } + + _, _, err = getKubernetesDelegate(clientInfo, net, tmpDir, fakePod, nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get resourceMap from DRA client")) + Expect(err.Error()).To(ContainSubstring("not found")) + }) + }) + + Context("when using getNetDelegate with DRA resources", func() { + It("should retrieve delegates with DRA resources populated", func() { + const fakePodName string = "netdelegate-dra-pod" + const fakeNamespace string = "default" + + // Create network attachment definition + netAttachDef := `{ + "name": "netdelegate-network", + "type": "ipvlan", + "cniVersion": "0.3.1" + }` + + // Create fake pod with DRA resource claims + claimName := "netdelegate-claim" + claimNamePtr := &claimName + fakePod := testutils.NewFakePod(fakePodName, "", "") + fakePod.Namespace = fakeNamespace + fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{ + { + ResourceClaimName: claimNamePtr, + }, + } + + // Setup client + clientInfo := NewFakeClientInfo() + _, err = clientInfo.AddPod(fakePod) + Expect(err).NotTo(HaveOccurred()) + + // Create network attachment definition + nad := testutils.NewFakeNetAttachDef(fakeNamespace, "netdelegate-network", netAttachDef) + _, err = clientInfo.AddNetAttachDef(nad) + Expect(err).NotTo(HaveOccurred()) + + // Create DRA resources + deviceName := "ipvlan-device" + driverName := "ipvlan.example.com" + poolName := "ipvlan-pool" + requestName := "vnic" + deviceID := "pci:0000:00:04.0" + + deviceIDValue := deviceID + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ipvlan-resource-slice", + }, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{ + Name: poolName, + ResourceSliceCount: 1, + }, + Devices: []resourcev1api.Device{ + { + Name: deviceName, + Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + "k8s.cni.cncf.io/deviceID": { + StringValue: &deviceIDValue, + }, + }, + }, + }, + }, + } + + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: fakeNamespace, + }, + Status: resourcev1api.ResourceClaimStatus{ + Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{ + Results: []resourcev1api.DeviceRequestAllocationResult{ + { + Request: requestName, + Driver: driverName, + Pool: poolName, + Device: deviceName, + }, + }, + }, + }, + }, + } + + // Add DRA resources to fake client + _, err = clientInfo.Client.ResourceV1().ResourceClaims(fakeNamespace).Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = clientInfo.Client.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + // Call getNetDelegate + resourceMap := make(map[string]*types.ResourceInfo) + delegate, updatedResourceMap, err := getNetDelegate(clientInfo, fakePod, "netdelegate-network", tmpDir, fakeNamespace, resourceMap) + Expect(err).NotTo(HaveOccurred()) + Expect(delegate).NotTo(BeNil()) + Expect(updatedResourceMap).NotTo(BeNil()) + }) + }) + + Context("when pod has both device-plugin resources and a DRA ResourceClaim", func() { + // Both tests override getResourceClientFunc so the kubelet stub returns a pre-seeded + // SR-IOV device-plugin allocation, letting us exercise the combined kubelet+DRA path. + const sriovResourceName = "openshift/sriov" + const sriovDeviceIDKubelet = "0000:01:00.4" + var origGetResourceClient func(string) (types.ResourceClient, error) + + BeforeEach(func() { + origGetResourceClient = getResourceClientFunc + getResourceClientFunc = func(string) (types.ResourceClient, error) { + return &fakeResourceClient{ + resourceMap: map[string]*types.ResourceInfo{ + sriovResourceName: {DeviceIDs: []string{sriovDeviceIDKubelet}}, + }, + }, nil + } + }) + + AfterEach(func() { + getResourceClientFunc = origGetResourceClient + }) + + It("should merge kubelet device-plugin entry with DRA SR-IOV claim that has full CNI attributes", func() { + const fakePodName = "hybrid-sriov-pod" + const fakeNamespace = "default" + const sriovDeviceIDDRA = "0000:02:00.4" + + netAttachDef := `{"name":"sriov-net","type":"sriov","cniVersion":"0.3.1"}` + claimName := "sriov-dra-claim" + driverName := "sriovnetwork.k8snetworkplumbingwg.io" + poolName := "sriov-pool" + deviceName := "vf-0" + + claimNamePtr := claimName + fakePod := testutils.NewFakePod(fakePodName, "sriov-net", "") + fakePod.Namespace = fakeNamespace + fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{ + {Name: claimName, ResourceClaimName: &claimNamePtr}, + } + + clientInfo := NewFakeClientInfo() + _, err = clientInfo.AddPod(fakePod) + Expect(err).NotTo(HaveOccurred()) + + // NAD carries the resourceName annotation → triggers both kubelet and DRA paths + nad := testutils.NewFakeNetAttachDef(fakeNamespace, "sriov-net", netAttachDef) + nad.Annotations = map[string]string{resourceNameAnnot: sriovResourceName} + _, err = clientInfo.AddNetAttachDef(nad) + Expect(err).NotTo(HaveOccurred()) + + // DRA device has both CNI attributes → appended to kubelet entry in resourceMap + draDeviceID := sriovDeviceIDDRA + draResName := sriovResourceName + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "sriov-dra-slice"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + {Name: deviceName, Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + "k8s.cni.cncf.io/deviceID": {StringValue: &draDeviceID}, + "k8s.cni.cncf.io/resourceName": {StringValue: &draResName}, + }}, + }, + }, + } + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claimName, Namespace: fakeNamespace}, + Status: resourcev1api.ResourceClaimStatus{Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: "sriov", Driver: driverName, Pool: poolName, Device: deviceName}, + }}, + }}, + } + + _, err = clientInfo.Client.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = clientInfo.Client.ResourceV1().ResourceClaims(fakeNamespace).Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + net := &types.NetworkSelectionElement{Name: "sriov-net", Namespace: fakeNamespace} + delegate, resourceMap, err := getKubernetesDelegate(clientInfo, net, tmpDir, fakePod, nil) + Expect(err).NotTo(HaveOccurred()) + Expect(delegate).NotTo(BeNil()) + + // Both kubelet and DRA device IDs must be present under the same resource name key + Expect(resourceMap).To(HaveKey(sriovResourceName)) + Expect(resourceMap[sriovResourceName].DeviceIDs).To(ConsistOf(sriovDeviceIDKubelet, sriovDeviceIDDRA)) + }) + + It("should silently skip a GPU DRA claim that has no CNI attributes and still resolve the device-plugin SR-IOV entry", func() { + const fakePodName = "hybrid-gpu-pod" + const fakeNamespace = "default" + + netAttachDef := `{"name":"sriov-net","type":"sriov","cniVersion":"0.3.1"}` + claimName := "gpu-dra-claim" + driverName := "gpu.nvidia.com" + poolName := "gpu-pool" + deviceName := "gpu-0" + + claimNamePtr := claimName + fakePod := testutils.NewFakePod(fakePodName, "sriov-net", "") + fakePod.Namespace = fakeNamespace + fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{ + {Name: claimName, ResourceClaimName: &claimNamePtr}, + } + + clientInfo := NewFakeClientInfo() + _, err = clientInfo.AddPod(fakePod) + Expect(err).NotTo(HaveOccurred()) + + nad := testutils.NewFakeNetAttachDef(fakeNamespace, "sriov-net", netAttachDef) + nad.Annotations = map[string]string{resourceNameAnnot: sriovResourceName} + _, err = clientInfo.AddNetAttachDef(nad) + Expect(err).NotTo(HaveOccurred()) + + // GPU ResourceSlice intentionally has no k8s.cni.cncf.io/deviceID attribute + gpuAttrVal := "gpu-model-a100" + resourceSlice := &resourcev1api.ResourceSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "gpu-dra-slice"}, + Spec: resourcev1api.ResourceSliceSpec{ + Driver: driverName, + Pool: resourcev1api.ResourcePool{Name: poolName, ResourceSliceCount: 1}, + Devices: []resourcev1api.Device{ + {Name: deviceName, Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{ + "gpu.nvidia.com/model": {StringValue: &gpuAttrVal}, + }}, + }, + }, + } + resourceClaim := &resourcev1api.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: claimName, Namespace: fakeNamespace}, + Status: resourcev1api.ResourceClaimStatus{Allocation: &resourcev1api.AllocationResult{ + Devices: resourcev1api.DeviceAllocationResult{Results: []resourcev1api.DeviceRequestAllocationResult{ + {Request: "gpu", Driver: driverName, Pool: poolName, Device: deviceName}, + }}, + }}, + } + + _, err = clientInfo.Client.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + _, err = clientInfo.Client.ResourceV1().ResourceClaims(fakeNamespace).Create(context.TODO(), resourceClaim, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + net := &types.NetworkSelectionElement{Name: "sriov-net", Namespace: fakeNamespace} + delegate, resourceMap, err := getKubernetesDelegate(clientInfo, net, tmpDir, fakePod, nil) + Expect(err).NotTo(HaveOccurred()) + Expect(delegate).NotTo(BeNil()) + + // GPU claim contributed nothing; only the kubelet device-plugin entry is present + Expect(resourceMap).To(HaveKey(sriovResourceName)) + Expect(resourceMap[sriovResourceName].DeviceIDs).To(Equal([]string{sriovDeviceIDKubelet})) + }) + }) + }) }) diff --git a/pkg/kubeletclient/kubeletclient.go b/pkg/kubeletclient/kubeletclient.go index 0b7bc5d2a..5398f1802 100644 --- a/pkg/kubeletclient/kubeletclient.go +++ b/pkg/kubeletclient/kubeletclient.go @@ -21,7 +21,6 @@ import ( "net/url" "os" "path/filepath" - "strings" "time" "golang.org/x/net/context" @@ -111,7 +110,6 @@ type kubeletClient struct { } func (rc *kubeletClient) getPodResources(client podresourcesapi.PodResourcesListerClient) error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -139,7 +137,6 @@ func (rc *kubeletClient) GetPodResourceMap(pod *v1.Pod) (map[string]*types.Resou if pr.Name == name && pr.Namespace == ns { for _, cnt := range pr.Containers { rc.getDevicePluginResources(cnt.Devices, resourceMap) - rc.getDRAResources(cnt.DynamicResources, resourceMap) } } } @@ -157,27 +154,6 @@ func (rc *kubeletClient) getDevicePluginResources(devices []*podresourcesapi.Con } } -func (rc *kubeletClient) getDRAResources(dynamicResources []*podresourcesapi.DynamicResource, resourceMap map[string]*types.ResourceInfo) { - for _, dynamicResource := range dynamicResources { - var deviceIDs []string - for _, claimResource := range dynamicResource.ClaimResources { - for _, cdiDevice := range claimResource.CdiDevices { - res := strings.Split(cdiDevice.Name, "=") - if len(res) == 2 { - deviceIDs = append(deviceIDs, res[1]) - } else { - logging.Errorf("GetPodResourceMap: Invalid CDI format") - } - } - } - if rInfo, ok := resourceMap[dynamicResource.ClaimName]; ok { - rInfo.DeviceIDs = append(rInfo.DeviceIDs, deviceIDs...) - } else { - resourceMap[dynamicResource.ClaimName] = &types.ResourceInfo{DeviceIDs: deviceIDs} - } - } -} - func hasKubeletAPIEndpoint(url *url.URL) bool { // Check for kubelet resource API socket file if _, err := os.Stat(url.Path); err != nil { diff --git a/pkg/kubeletclient/kubeletclient_test.go b/pkg/kubeletclient/kubeletclient_test.go index a9593a1c1..ca2097988 100644 --- a/pkg/kubeletclient/kubeletclient_test.go +++ b/pkg/kubeletclient/kubeletclient_test.go @@ -46,8 +46,8 @@ var ( ) type fakeResourceServer struct { - server *grpc.Server podresourcesapi.UnimplementedPodResourcesListerServer + server *grpc.Server } // TODO: This is stub code for test, but we may need to change for the testing we use this API in the future... @@ -68,32 +68,6 @@ func (m *fakeResourceServer) List(_ context.Context, _ *podresourcesapi.ListPodR }, } - cdiDevices := []*podresourcesapi.CDIDevice{ - { - Name: "cdi-kind=cdi-resource", - }, - } - draDriverName := "dra.example.com" - poolName := "worker-1-pool" - deviceName := "gpu-1" - - claimsResource := []*podresourcesapi.ClaimResource{ - { - CdiDevices: cdiDevices, - DriverName: draDriverName, - PoolName: poolName, - DeviceName: deviceName, - }, - } - - dynamicResources := []*podresourcesapi.DynamicResource{ - { - ClaimName: "resource-claim", - ClaimNamespace: "dynamic-resource-pod-namespace", - ClaimResources: claimsResource, - }, - } - resp := &podresourcesapi.ListPodResourcesResponse{ PodResources: []*podresourcesapi.PodResources{ { @@ -106,16 +80,6 @@ func (m *fakeResourceServer) List(_ context.Context, _ *podresourcesapi.ListPodR }, }, }, - { - Name: "dynamic-resource-pod-name", - Namespace: "dynamic-resource-pod-namespace", - Containers: []*podresourcesapi.ContainerResources{ - { - Name: "dynamic-resource-container-name", - DynamicResources: dynamicResources, - }, - }, - }, }, } return resp, nil @@ -249,34 +213,6 @@ var _ = Describe("Kubelet resource endpoint data read operations", func() { Expect(resourceMap).To(Equal(outputRMap)) }) - It("should return no error with dynamic resource", func() { - podUID := k8sTypes.UID("9f94e27b-4233-43d6-bd10-f73b4de6f456") - fakePod := &v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "dynamic-resource-pod-name", - Namespace: "dynamic-resource-pod-namespace", - UID: podUID, - }, - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "dynamic-resource-container-name", - }, - }, - }, - } - client, err := getKubeletClient(testKubeletSocket) - Expect(err).NotTo(HaveOccurred()) - - outputRMap := map[string]*mtypes.ResourceInfo{ - "resource-claim": {DeviceIDs: []string{"cdi-resource"}}, - } - resourceMap, err := client.GetPodResourceMap(fakePod) - Expect(err).NotTo(HaveOccurred()) - Expect(resourceMap).ShouldNot(BeNil()) - Expect(resourceMap).To(Equal(outputRMap)) - }) - It("should return an error with garbage socket value", func() { u, err := url.Parse("/badfilepath!?//") Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index 5996a07af..b5c18209d 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -138,6 +138,11 @@ func Errorf(format string, a ...interface{}) error { return fmt.Errorf(format, a...) } +// Warningf prints a warning at the same visibility threshold as Errorf (prefix "warning:"). +func Warningf(format string, a ...interface{}) { + printf(ErrorLevel, "warning: "+format, a...) +} + // Panicf prints logging plus stack trace. This should be used only for unrecoverable error func Panicf(format string, a ...interface{}) { printf(PanicLevel, format, a...) diff --git a/pkg/multus/multus.go b/pkg/multus/multus.go index a54854ba4..1dedf8575 100644 --- a/pkg/multus/multus.go +++ b/pkg/multus/multus.go @@ -622,7 +622,7 @@ func delPlugins(exec invoke.Exec, pod *v1.Pod, args *skel.CmdArgs, k8sArgs *type // Check if we had any errors, and send them all back. if len(errorstrings) > 0 { - return fmt.Errorf("%s", strings.Join(errorstrings, " / ")) + return fmt.Errorf("errors: %s", strings.Join(errorstrings, " / ")) } return nil diff --git a/pkg/server/server.go b/pkg/server/server.go index a7d81ebbd..6163539b8 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -51,7 +51,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/apimachinery/pkg/util/sets" utilwait "k8s.io/apimachinery/pkg/util/wait" informerfactory "k8s.io/client-go/informers" v1coreinformers "k8s.io/client-go/informers/core/v1" @@ -479,9 +478,16 @@ func (s *Server) handleDelegateRequest(r *http.Request) ([]byte, error) { } func overrideCNIConfigWithServerConfig(cniConf []byte, overrideConf []byte, ignoreReadinessIndicator bool) ([]byte, error) { - if len(overrideConf) == 0 { + // If there is no server-side override config AND we don't need to strip any keys, + // return the client config unchanged. + if len(overrideConf) == 0 && !ignoreReadinessIndicator { return cniConf, nil } + // Treat a missing server config as an empty object so the key-stripping logic below + // still runs when ignoreReadinessIndicator is true. + if len(overrideConf) == 0 { + overrideConf = []byte("{}") + } var cni map[string]interface{} if err := json.Unmarshal(cniConf, &cni); err != nil { @@ -493,14 +499,13 @@ func overrideCNIConfigWithServerConfig(cniConf []byte, overrideConf []byte, igno return nil, fmt.Errorf("failed to unmarshall CNI override config: %w", err) } - // Copy each key of the override config into the CNI config except for - // a few specific keys - ignoreKeys := sets.NewString() + // Remove keys from the client config that the server wants to ignore, then + // overlay the server-side overrides (also skipping those same keys). if ignoreReadinessIndicator { - ignoreKeys.Insert("readinessindicatorfile") + delete(cni, "readinessindicatorfile") } for overrideKey, overrideVal := range override { - if !ignoreKeys.Has(overrideKey) { + if !ignoreReadinessIndicator || overrideKey != "readinessindicatorfile" { cni[overrideKey] = overrideVal } }