k8s.io/kubernetes@v1.29.3/pkg/kubelet/cm/devicemanager/pod_devices_test.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package devicemanager 18 19 import ( 20 "encoding/json" 21 "testing" 22 23 "github.com/stretchr/testify/assert" 24 "github.com/stretchr/testify/require" 25 26 "k8s.io/apimachinery/pkg/util/sets" 27 utilfeature "k8s.io/apiserver/pkg/util/feature" 28 featuregatetesting "k8s.io/component-base/featuregate/testing" 29 pluginapi "k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1" 30 "k8s.io/kubernetes/pkg/features" 31 "k8s.io/kubernetes/pkg/kubelet/cm/devicemanager/checkpoint" 32 kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" 33 ) 34 35 func TestGetContainerDevices(t *testing.T) { 36 podDevices := newPodDevices() 37 resourceName1 := "domain1.com/resource1" 38 podID := "pod1" 39 contID := "con1" 40 devices := checkpoint.DevicesPerNUMA{0: []string{"dev1"}, 1: []string{"dev1"}} 41 42 podDevices.insert(podID, contID, resourceName1, 43 devices, 44 newContainerAllocateResponse( 45 withDevices(map[string]string{"/dev/r1dev1": "/dev/r1dev1", "/dev/r1dev2": "/dev/r1dev2"}), 46 withMounts(map[string]string{"/home/r1lib1": "/usr/r1lib1"}), 47 ), 48 ) 49 50 resContDevices := podDevices.getContainerDevices(podID, contID) 51 contDevices, ok := resContDevices[resourceName1] 52 require.True(t, ok, "resource %q not present", resourceName1) 53 54 for devID, plugInfo := range contDevices { 55 nodes := plugInfo.GetTopology().GetNodes() 56 require.Equal(t, len(nodes), len(devices), "Incorrect container devices: %v - %v (nodes %v)", devices, contDevices, nodes) 57 58 for _, node := range plugInfo.GetTopology().GetNodes() { 59 dev, ok := devices[node.ID] 60 require.True(t, ok, "NUMA id %v doesn't exist in result", node.ID) 61 require.Equal(t, devID, dev[0], "Can't find device %s in result", dev[0]) 62 } 63 } 64 } 65 66 func TestResourceDeviceInstanceFilter(t *testing.T) { 67 var expected string 68 var cond map[string]sets.Set[string] 69 var resp ResourceDeviceInstances 70 devs := ResourceDeviceInstances{ 71 "foo": DeviceInstances{ 72 "dev-foo1": pluginapi.Device{ 73 ID: "foo1", 74 }, 75 "dev-foo2": pluginapi.Device{ 76 ID: "foo2", 77 }, 78 "dev-foo3": pluginapi.Device{ 79 ID: "foo3", 80 }, 81 }, 82 "bar": DeviceInstances{ 83 "dev-bar1": pluginapi.Device{ 84 ID: "bar1", 85 }, 86 "dev-bar2": pluginapi.Device{ 87 ID: "bar2", 88 }, 89 "dev-bar3": pluginapi.Device{ 90 ID: "bar3", 91 }, 92 }, 93 "baz": DeviceInstances{ 94 "dev-baz1": pluginapi.Device{ 95 ID: "baz1", 96 }, 97 "dev-baz2": pluginapi.Device{ 98 ID: "baz2", 99 }, 100 "dev-baz3": pluginapi.Device{ 101 ID: "baz3", 102 }, 103 }, 104 } 105 106 resp = devs.Filter(map[string]sets.Set[string]{}) 107 expected = `{}` 108 expectResourceDeviceInstances(t, resp, expected) 109 110 cond = map[string]sets.Set[string]{ 111 "foo": sets.New[string]("dev-foo1", "dev-foo2"), 112 "bar": sets.New[string]("dev-bar1"), 113 } 114 resp = devs.Filter(cond) 115 expected = `{"bar":{"dev-bar1":{"ID":"bar1"}},"foo":{"dev-foo1":{"ID":"foo1"},"dev-foo2":{"ID":"foo2"}}}` 116 expectResourceDeviceInstances(t, resp, expected) 117 118 cond = map[string]sets.Set[string]{ 119 "foo": sets.New[string]("dev-foo1", "dev-foo2", "dev-foo3"), 120 "bar": sets.New[string]("dev-bar1", "dev-bar2", "dev-bar3"), 121 "baz": sets.New[string]("dev-baz1", "dev-baz2", "dev-baz3"), 122 } 123 resp = devs.Filter(cond) 124 expected = `{"bar":{"dev-bar1":{"ID":"bar1"},"dev-bar2":{"ID":"bar2"},"dev-bar3":{"ID":"bar3"}},"baz":{"dev-baz1":{"ID":"baz1"},"dev-baz2":{"ID":"baz2"},"dev-baz3":{"ID":"baz3"}},"foo":{"dev-foo1":{"ID":"foo1"},"dev-foo2":{"ID":"foo2"},"dev-foo3":{"ID":"foo3"}}}` 125 expectResourceDeviceInstances(t, resp, expected) 126 127 cond = map[string]sets.Set[string]{ 128 "foo": sets.New[string]("dev-foo1", "dev-foo2", "dev-foo3", "dev-foo4"), 129 "bar": sets.New[string]("dev-bar1", "dev-bar2", "dev-bar3", "dev-bar4"), 130 "baz": sets.New[string]("dev-baz1", "dev-baz2", "dev-baz3", "dev-bar4"), 131 } 132 resp = devs.Filter(cond) 133 expected = `{"bar":{"dev-bar1":{"ID":"bar1"},"dev-bar2":{"ID":"bar2"},"dev-bar3":{"ID":"bar3"}},"baz":{"dev-baz1":{"ID":"baz1"},"dev-baz2":{"ID":"baz2"},"dev-baz3":{"ID":"baz3"}},"foo":{"dev-foo1":{"ID":"foo1"},"dev-foo2":{"ID":"foo2"},"dev-foo3":{"ID":"foo3"}}}` 134 expectResourceDeviceInstances(t, resp, expected) 135 136 cond = map[string]sets.Set[string]{ 137 "foo": sets.New[string]("dev-foo1", "dev-foo4", "dev-foo7"), 138 "bar": sets.New[string]("dev-bar1", "dev-bar4", "dev-bar7"), 139 "baz": sets.New[string]("dev-baz1", "dev-baz4", "dev-baz7"), 140 } 141 resp = devs.Filter(cond) 142 expected = `{"bar":{"dev-bar1":{"ID":"bar1"}},"baz":{"dev-baz1":{"ID":"baz1"}},"foo":{"dev-foo1":{"ID":"foo1"}}}` 143 expectResourceDeviceInstances(t, resp, expected) 144 145 } 146 147 func expectResourceDeviceInstances(t *testing.T, resp ResourceDeviceInstances, expected string) { 148 // per docs in https://pkg.go.dev/encoding/json#Marshal 149 // "Map values encode as JSON objects. The map's key type must either be a string, an integer type, or 150 // implement encoding.TextMarshaler. The map keys are sorted [...]" 151 // so this check is expected to be stable and not flaky 152 data, err := json.Marshal(resp) 153 if err != nil { 154 t.Fatalf("unexpected JSON marshalling error: %v", err) 155 } 156 got := string(data) 157 if got != expected { 158 t.Errorf("expected %q got %q", expected, got) 159 } 160 } 161 162 func TestDeviceRunContainerOptions(t *testing.T) { 163 const ( 164 podUID = "pod" 165 containerName = "container" 166 resource1 = "example1.com/resource1" 167 resource2 = "example2.com/resource2" 168 ) 169 testCases := []struct { 170 description string 171 gate bool 172 responsesPerResource map[string]*pluginapi.ContainerAllocateResponse 173 expected *DeviceRunContainerOptions 174 }{ 175 { 176 description: "empty response", 177 gate: false, 178 responsesPerResource: map[string]*pluginapi.ContainerAllocateResponse{ 179 resource1: newContainerAllocateResponse(), 180 }, 181 expected: &DeviceRunContainerOptions{}, 182 }, 183 { 184 description: "cdi devices are ingored when feature gate is disabled", 185 gate: false, 186 responsesPerResource: map[string]*pluginapi.ContainerAllocateResponse{ 187 resource1: newContainerAllocateResponse( 188 withDevices(map[string]string{"/dev/r1": "/dev/r1"}), 189 withMounts(map[string]string{"/home/lib1": "/home/lib1"}), 190 withEnvs(map[string]string{"ENV1": "VALUE1"}), 191 withCDIDevices("vendor1.com/class1=device1", "vendor2.com/class2=device2"), 192 ), 193 }, 194 expected: &DeviceRunContainerOptions{ 195 Devices: []kubecontainer.DeviceInfo{ 196 {PathOnHost: "/dev/r1", PathInContainer: "/dev/r1", Permissions: "mrw"}, 197 }, 198 Mounts: []kubecontainer.Mount{ 199 {Name: "/home/lib1", HostPath: "/home/lib1", ContainerPath: "/home/lib1", ReadOnly: true}, 200 }, 201 Envs: []kubecontainer.EnvVar{ 202 {Name: "ENV1", Value: "VALUE1"}, 203 }, 204 }, 205 }, 206 { 207 description: "cdi devices are handled when feature gate is enabled", 208 gate: true, 209 responsesPerResource: map[string]*pluginapi.ContainerAllocateResponse{ 210 resource1: newContainerAllocateResponse( 211 withCDIDevices("vendor1.com/class1=device1", "vendor2.com/class2=device2"), 212 ), 213 }, 214 expected: &DeviceRunContainerOptions{ 215 Annotations: []kubecontainer.Annotation{ 216 {Name: "cdi.k8s.io/devicemanager_pod-container", Value: "vendor1.com/class1=device1,vendor2.com/class2=device2"}, 217 }, 218 CDIDevices: []kubecontainer.CDIDevice{ 219 {Name: "vendor1.com/class1=device1"}, 220 {Name: "vendor2.com/class2=device2"}, 221 }, 222 }, 223 }, 224 { 225 description: "cdi devices from multiple resources are handled when feature gate is enabled", 226 gate: true, 227 responsesPerResource: map[string]*pluginapi.ContainerAllocateResponse{ 228 resource1: newContainerAllocateResponse( 229 withCDIDevices("vendor1.com/class1=device1", "vendor2.com/class2=device2"), 230 ), 231 resource2: newContainerAllocateResponse( 232 withCDIDevices("vendor3.com/class3=device3", "vendor4.com/class4=device4"), 233 ), 234 }, 235 expected: &DeviceRunContainerOptions{ 236 Annotations: []kubecontainer.Annotation{ 237 {Name: "cdi.k8s.io/devicemanager_pod-container", Value: "vendor1.com/class1=device1,vendor2.com/class2=device2,vendor3.com/class3=device3,vendor4.com/class4=device4"}, 238 }, 239 CDIDevices: []kubecontainer.CDIDevice{ 240 {Name: "vendor1.com/class1=device1"}, 241 {Name: "vendor2.com/class2=device2"}, 242 {Name: "vendor3.com/class3=device3"}, 243 {Name: "vendor4.com/class4=device4"}, 244 }, 245 }, 246 }, 247 { 248 description: "duplicate cdi devices are skipped", 249 gate: true, 250 responsesPerResource: map[string]*pluginapi.ContainerAllocateResponse{ 251 resource1: newContainerAllocateResponse( 252 withCDIDevices("vendor1.com/class1=device1", "vendor2.com/class2=device2"), 253 ), 254 resource2: newContainerAllocateResponse( 255 withCDIDevices("vendor2.com/class2=device2", "vendor3.com/class3=device3"), 256 ), 257 }, 258 expected: &DeviceRunContainerOptions{ 259 Annotations: []kubecontainer.Annotation{ 260 {Name: "cdi.k8s.io/devicemanager_pod-container", Value: "vendor1.com/class1=device1,vendor2.com/class2=device2,vendor3.com/class3=device3"}, 261 }, 262 CDIDevices: []kubecontainer.CDIDevice{ 263 {Name: "vendor1.com/class1=device1"}, 264 {Name: "vendor2.com/class2=device2"}, 265 {Name: "vendor3.com/class3=device3"}, 266 }, 267 }, 268 }, 269 } 270 271 for _, tc := range testCases { 272 t.Run(tc.description, func(t *testing.T) { 273 as := assert.New(t) 274 275 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DevicePluginCDIDevices, tc.gate)() 276 podDevices := newPodDevices() 277 for resourceName, response := range tc.responsesPerResource { 278 podDevices.insert("pod", "container", resourceName, 279 nil, 280 response, 281 ) 282 } 283 opts := podDevices.deviceRunContainerOptions(podUID, containerName) 284 285 // The exact ordering of the options depends on the order of the resources in the map. 286 // We therefore use `ElementsMatch` instead of `Equal` on the member slices. 287 as.ElementsMatch(tc.expected.Annotations, opts.Annotations) 288 as.ElementsMatch(tc.expected.CDIDevices, opts.CDIDevices) 289 as.ElementsMatch(tc.expected.Devices, opts.Devices) 290 as.ElementsMatch(tc.expected.Envs, opts.Envs) 291 as.ElementsMatch(tc.expected.Mounts, opts.Mounts) 292 }) 293 } 294 }