@@ -18,23 +18,26 @@ package driver
1818
1919import (
2020 "context"
21- "fmt"
22- "strings"
23- "testing"
24-
2521 "encoding/json"
22+ "fmt"
2623 "net/http"
2724 "net/http/httptest"
25+ "strings"
26+ "syscall"
27+ "testing"
2828
2929 "github.com/google/go-cmp/cmp"
30+ "github.com/google/go-cmp/cmp/cmpopts"
3031 "github.com/prometheus/client_golang/prometheus/testutil"
32+ "github.com/vishvananda/netlink"
3133 resourcev1 "k8s.io/api/resource/v1"
3234 k8sresource "k8s.io/apimachinery/pkg/api/resource"
3335 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3436 "k8s.io/apimachinery/pkg/types"
3537 "k8s.io/client-go/tools/record"
3638 "k8s.io/dynamic-resource-allocation/kubeletplugin"
3739 "k8s.io/utils/ptr"
40+ userns "sigs.k8s.io/dranet/internal/testutils"
3841 "sigs.k8s.io/dranet/pkg/apis"
3942 "sigs.k8s.io/dranet/pkg/cloudprovider"
4043 "sigs.k8s.io/dranet/pkg/cloudprovider/webhook"
@@ -954,3 +957,302 @@ func TestMergeDevices(t *testing.T) {
954957 })
955958 }
956959}
960+
961+ // TODO: To further improve test coverage, consider constructing and mounting fake
962+ // sysfs paths for RDMA character devices (e.g., /dev/infiniband/uverbs0). This
963+ // would allow testing of device discovery and character device aggregation logic
964+ // that currently depends on the host's physical hardware.
965+ func TestPrepareResourceClaim (t * testing.T ) {
966+ userns .Run (t , testPrepareResourceClaim_Namespaced , syscall .CLONE_NEWNET )
967+ }
968+
969+ func testPrepareResourceClaim_Namespaced (t * testing.T ) {
970+ ctx := t .Context ()
971+ const testDriverName = "test.driver"
972+
973+ // We are in a fresh, isolated netns for all these test cases.
974+ // Create a shared dummy interface that tests can rely on.
975+ la := netlink .NewLinkAttrs ()
976+ la .Name = "dummy0"
977+ dummy := & netlink.Dummy {LinkAttrs : la }
978+ if err := netlink .LinkAdd (dummy ); err != nil && ! strings .Contains (err .Error (), "file exists" ) {
979+ t .Fatalf ("Failed to create shared dummy interface: %v" , err )
980+ }
981+
982+ testCases := []struct {
983+ name string
984+ claim * resourcev1.ResourceClaim
985+ setupDB func (* fakeInventoryDB )
986+ wantErr string
987+ wantPodConfig * PodConfig
988+ }{
989+ {
990+ name : "single IB-only device builds RDMA config successfully" ,
991+ claim : & resourcev1.ResourceClaim {
992+ ObjectMeta : metav1.ObjectMeta {UID : "claim-uid-ib-single" , Namespace : "default" , Name : "claim-ib-single" },
993+ Status : resourcev1.ResourceClaimStatus {
994+ ReservedFor : []resourcev1.ResourceClaimConsumerReference {
995+ {APIGroup : "" , Resource : "pods" , Name : "test-pod" , UID : "pod-uid-ib-single" },
996+ },
997+ Allocation : & resourcev1.AllocationResult {
998+ Devices : resourcev1.DeviceAllocationResult {
999+ Results : []resourcev1.DeviceRequestAllocationResult {
1000+ {Driver : testDriverName , Device : "ib-dev-0" , Request : "req-0" },
1001+ },
1002+ },
1003+ },
1004+ },
1005+ },
1006+ setupDB : func (db * fakeInventoryDB ) {
1007+ db .IsIBOnlyDeviceFunc = func (deviceName string ) bool { return true }
1008+ db .GetRDMADeviceNameFunc = func (deviceName string ) (string , error ) {
1009+ return "fake_mlx5_0" , nil
1010+ }
1011+ db .GetDeviceFunc = func (deviceName string ) (resourcev1.Device , bool ) {
1012+ return resourcev1.Device {Name : deviceName }, true
1013+ }
1014+ },
1015+ wantPodConfig : & PodConfig {
1016+ DeviceConfigs : map [string ]DeviceConfig {
1017+ "ib-dev-0" : {
1018+ Claim : types.NamespacedName {
1019+ Namespace : "default" ,
1020+ Name : "claim-ib-single" ,
1021+ },
1022+ DeviceSnapshot : & resourcev1.Device {Name : "ib-dev-0" },
1023+ RDMADevice : RDMAConfig {
1024+ LinkDev : "fake_mlx5_0" ,
1025+ },
1026+ },
1027+ },
1028+ },
1029+ },
1030+ {
1031+ name : "multiple IB-only devices in single claim build independent RDMA configs without accumulation" ,
1032+ claim : & resourcev1.ResourceClaim {
1033+ ObjectMeta : metav1.ObjectMeta {UID : "claim-uid-ib-multi" , Namespace : "default" , Name : "claim-ib-multi" },
1034+ Status : resourcev1.ResourceClaimStatus {
1035+ ReservedFor : []resourcev1.ResourceClaimConsumerReference {
1036+ {APIGroup : "" , Resource : "pods" , Name : "test-pod" , UID : "pod-uid-ib-multi" },
1037+ },
1038+ Allocation : & resourcev1.AllocationResult {
1039+ Devices : resourcev1.DeviceAllocationResult {
1040+ Results : []resourcev1.DeviceRequestAllocationResult {
1041+ // Two requests for two separate IB devices within the same claim
1042+ {Driver : testDriverName , Device : "ib-dev-0" , Request : "req-0" },
1043+ {Driver : testDriverName , Device : "ib-dev-1" , Request : "req-1" },
1044+ },
1045+ },
1046+ },
1047+ },
1048+ },
1049+ setupDB : func (db * fakeInventoryDB ) {
1050+ db .IsIBOnlyDeviceFunc = func (deviceName string ) bool { return true }
1051+ db .GetRDMADeviceNameFunc = func (deviceName string ) (string , error ) {
1052+ switch deviceName {
1053+ case "ib-dev-0" :
1054+ return "fake_mlx5_0" , nil
1055+ case "ib-dev-1" :
1056+ return "fake_mlx5_1" , nil
1057+ default :
1058+ return "" , fmt .Errorf ("unexpected device %s" , deviceName )
1059+ }
1060+ }
1061+ db .GetDeviceFunc = func (deviceName string ) (resourcev1.Device , bool ) {
1062+ return resourcev1.Device {Name : deviceName }, true
1063+ }
1064+ },
1065+ wantPodConfig : & PodConfig {
1066+ DeviceConfigs : map [string ]DeviceConfig {
1067+ "ib-dev-0" : {
1068+ Claim : types.NamespacedName {
1069+ Namespace : "default" ,
1070+ Name : "claim-ib-multi" ,
1071+ },
1072+ DeviceSnapshot : & resourcev1.Device {Name : "ib-dev-0" },
1073+ RDMADevice : RDMAConfig {
1074+ LinkDev : "fake_mlx5_0" ,
1075+ },
1076+ },
1077+ "ib-dev-1" : {
1078+ Claim : types.NamespacedName {
1079+ Namespace : "default" ,
1080+ Name : "claim-ib-multi" ,
1081+ },
1082+ DeviceSnapshot : & resourcev1.Device {Name : "ib-dev-1" },
1083+ RDMADevice : RDMAConfig {
1084+ LinkDev : "fake_mlx5_1" ,
1085+ },
1086+ },
1087+ },
1088+ },
1089+ },
1090+ {
1091+ name : "single network device builds config successfully" ,
1092+ claim : & resourcev1.ResourceClaim {
1093+ ObjectMeta : metav1.ObjectMeta {UID : "claim-uid-net-single" , Namespace : "default" , Name : "claim-net-single" },
1094+ Status : resourcev1.ResourceClaimStatus {
1095+ ReservedFor : []resourcev1.ResourceClaimConsumerReference {
1096+ {APIGroup : "" , Resource : "pods" , Name : "test-pod" , UID : "pod-uid-net-single" },
1097+ },
1098+ Allocation : & resourcev1.AllocationResult {
1099+ Devices : resourcev1.DeviceAllocationResult {
1100+ Results : []resourcev1.DeviceRequestAllocationResult {
1101+ {Driver : testDriverName , Device : "net-dev-0" , Request : "req-0" },
1102+ },
1103+ },
1104+ },
1105+ },
1106+ },
1107+ setupDB : func (db * fakeInventoryDB ) {
1108+ db .IsIBOnlyDeviceFunc = func (deviceName string ) bool { return false }
1109+ // Return the shared 'dummy0' created at the start of the test
1110+ db .GetNetInterfaceNameFunc = func (deviceName string ) (string , error ) {
1111+ return "dummy0" , nil
1112+ }
1113+ db .GetDeviceFunc = func (deviceName string ) (resourcev1.Device , bool ) {
1114+ return resourcev1.Device {Name : deviceName }, true
1115+ }
1116+ },
1117+ wantPodConfig : & PodConfig {
1118+ DeviceConfigs : map [string ]DeviceConfig {
1119+ "net-dev-0" : {
1120+ Claim : types.NamespacedName {
1121+ Namespace : "default" ,
1122+ Name : "claim-net-single" ,
1123+ },
1124+ DeviceSnapshot : & resourcev1.Device {Name : "net-dev-0" },
1125+ NetworkInterfaceConfigInHost : apis.NetworkConfig {
1126+ Interface : apis.InterfaceConfig {
1127+ Name : "dummy0" ,
1128+ },
1129+ },
1130+ NetworkInterfaceConfigInPod : apis.NetworkConfig {
1131+ Interface : apis.InterfaceConfig {
1132+ Name : "dummy0" ,
1133+ },
1134+ },
1135+ },
1136+ },
1137+ },
1138+ },
1139+ {
1140+ name : "no pods allocated to claim" ,
1141+ claim : & resourcev1.ResourceClaim {
1142+ ObjectMeta : metav1.ObjectMeta {UID : "claim-uid-empty" , Namespace : "default" , Name : "claim-empty" },
1143+ Status : resourcev1.ResourceClaimStatus {
1144+ ReservedFor : []resourcev1.ResourceClaimConsumerReference {},
1145+ },
1146+ },
1147+ },
1148+ {
1149+ name : "multiple pods allocated to claim returns error" ,
1150+ claim : & resourcev1.ResourceClaim {
1151+ ObjectMeta : metav1.ObjectMeta {UID : "claim-uid-multi-pod" , Namespace : "default" , Name : "claim-multi-pod" },
1152+ Status : resourcev1.ResourceClaimStatus {
1153+ ReservedFor : []resourcev1.ResourceClaimConsumerReference {
1154+ {APIGroup : "" , Resource : "pods" , Name : "pod-1" , UID : "pod-uid-1" },
1155+ {APIGroup : "" , Resource : "pods" , Name : "pod-2" , UID : "pod-uid-2" },
1156+ },
1157+ },
1158+ },
1159+ wantErr : "driver only supports one pod per claim, got 2" ,
1160+ },
1161+ {
1162+ name : "unsupported consumer reference returns error" ,
1163+ claim : & resourcev1.ResourceClaim {
1164+ ObjectMeta : metav1.ObjectMeta {UID : "claim-uid-unsupported-ref" , Namespace : "default" , Name : "claim-unsupported-ref" },
1165+ Status : resourcev1.ResourceClaimStatus {
1166+ ReservedFor : []resourcev1.ResourceClaimConsumerReference {
1167+ {APIGroup : "apps" , Resource : "deployments" , Name : "dep-1" , UID : "dep-uid-1" },
1168+ },
1169+ },
1170+ },
1171+ wantErr : "driver only supports Pods" ,
1172+ },
1173+ {
1174+ name : "devices managed by other drivers are ignored" ,
1175+ claim : & resourcev1.ResourceClaim {
1176+ ObjectMeta : metav1.ObjectMeta {UID : "claim-uid-other-driver" , Namespace : "default" , Name : "claim-other-driver" },
1177+ Status : resourcev1.ResourceClaimStatus {
1178+ ReservedFor : []resourcev1.ResourceClaimConsumerReference {
1179+ {APIGroup : "" , Resource : "pods" , Name : "test-pod" , UID : "pod-uid-1" },
1180+ },
1181+ Allocation : & resourcev1.AllocationResult {
1182+ Devices : resourcev1.DeviceAllocationResult {
1183+ Results : []resourcev1.DeviceRequestAllocationResult {
1184+ // This result specifies a different driver, so it should be safely ignored
1185+ {Driver : "other.driver.io" , Device : "gpu-0" , Request : "gpu-req" },
1186+ },
1187+ },
1188+ },
1189+ },
1190+ },
1191+ },
1192+ {
1193+ name : "device interface lookup failure returns error" ,
1194+ claim : & resourcev1.ResourceClaim {
1195+ ObjectMeta : metav1.ObjectMeta {UID : "claim-uid-net-fail" , Namespace : "default" , Name : "claim-net-fail" },
1196+ Status : resourcev1.ResourceClaimStatus {
1197+ ReservedFor : []resourcev1.ResourceClaimConsumerReference {
1198+ {APIGroup : "" , Resource : "pods" , Name : "test-pod" , UID : "pod-uid-net-fail" },
1199+ },
1200+ Allocation : & resourcev1.AllocationResult {
1201+ Devices : resourcev1.DeviceAllocationResult {
1202+ Results : []resourcev1.DeviceRequestAllocationResult {
1203+ {Driver : testDriverName , Device : "net-dev-0" , Request : "req-0" },
1204+ },
1205+ },
1206+ },
1207+ },
1208+ },
1209+ setupDB : func (db * fakeInventoryDB ) {
1210+ db .IsIBOnlyDeviceFunc = func (deviceName string ) bool { return false }
1211+ // Simulate failure when retrieving the interface name
1212+ db .GetNetInterfaceNameFunc = func (deviceName string ) (string , error ) {
1213+ return "" , fmt .Errorf ("interface not found in inventory" )
1214+ }
1215+ },
1216+ wantErr : "failed to get network interface name for device net-dev-0" ,
1217+ },
1218+ }
1219+
1220+ for _ , tc := range testCases {
1221+ t .Run (tc .name , func (t * testing.T ) {
1222+ fakeDB := newFakeInventoryDB ()
1223+ if tc .setupDB != nil {
1224+ tc .setupDB (fakeDB )
1225+ }
1226+
1227+ np := & NetworkDriver {
1228+ netdb : fakeDB ,
1229+ driverName : testDriverName ,
1230+ podConfigStore : mustNewPodConfigStore (),
1231+ eventRecorder : record .NewFakeRecorder (100 ),
1232+ }
1233+
1234+ gotResult := np .prepareResourceClaim (ctx , tc .claim )
1235+
1236+ if tc .wantErr != "" {
1237+ if gotResult .Err == nil || ! strings .Contains (gotResult .Err .Error (), tc .wantErr ) {
1238+ t .Fatalf ("prepareResourceClaim() error = %v, want error containing %q" , gotResult .Err , tc .wantErr )
1239+ }
1240+ } else if gotResult .Err != nil {
1241+ t .Fatalf ("prepareResourceClaim() unexpected error = %v" , gotResult .Err )
1242+ }
1243+
1244+ var gotPodConfig * PodConfig
1245+ if len (tc .claim .Status .ReservedFor ) > 0 {
1246+ podUID := tc .claim .Status .ReservedFor [0 ].UID
1247+ if podCfg , ok := np .podConfigStore .GetPodConfig (podUID ); ok {
1248+ gotPodConfig = & podCfg
1249+ }
1250+ }
1251+
1252+ opts := []cmp.Option {cmpopts .EquateEmpty (), cmpopts .IgnoreFields (PodConfig {}, "LastNRIActivity" )}
1253+ if diff := cmp .Diff (tc .wantPodConfig , gotPodConfig , opts ... ); diff != "" {
1254+ t .Errorf ("PodConfig mismatch (-want +got):\n %s" , diff )
1255+ }
1256+ })
1257+ }
1258+ }
0 commit comments