device_plugin.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. /*
  2. Copyright 2017 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package e2e_node
  14. import (
  15. "fmt"
  16. "os"
  17. "path/filepath"
  18. "time"
  19. "regexp"
  20. "k8s.io/api/core/v1"
  21. "k8s.io/apimachinery/pkg/api/resource"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/util/uuid"
  24. "k8s.io/kubernetes/pkg/features"
  25. kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
  26. "k8s.io/kubernetes/test/e2e/framework"
  27. e2elog "k8s.io/kubernetes/test/e2e/framework/log"
  28. pluginapi "k8s.io/kubernetes/pkg/kubelet/apis/deviceplugin/v1beta1"
  29. dm "k8s.io/kubernetes/pkg/kubelet/cm/devicemanager"
  30. . "github.com/onsi/ginkgo"
  31. . "github.com/onsi/gomega"
  32. )
  33. const (
  34. // fake resource name
  35. resourceName = "fake.com/resource"
  36. )
  37. // Serial because the test restarts Kubelet
  38. var _ = framework.KubeDescribe("Device Plugin [Feature:DevicePluginProbe][NodeFeature:DevicePluginProbe][Serial]", func() {
  39. f := framework.NewDefaultFramework("device-plugin-errors")
  40. testDevicePlugin(f, "/var/lib/kubelet/plugins_registry")
  41. })
  42. func testDevicePlugin(f *framework.Framework, pluginSockDir string) {
  43. pluginSockDir = filepath.Join(pluginSockDir) + "/"
  44. Context("DevicePlugin", func() {
  45. By("Enabling support for Kubelet Plugins Watcher")
  46. tempSetCurrentKubeletConfig(f, func(initialConfig *kubeletconfig.KubeletConfiguration) {
  47. if initialConfig.FeatureGates == nil {
  48. initialConfig.FeatureGates = map[string]bool{}
  49. }
  50. initialConfig.FeatureGates[string(features.KubeletPodResources)] = true
  51. })
  52. It("Verifies the Kubelet device plugin functionality.", func() {
  53. By("Start stub device plugin")
  54. // fake devices for e2e test
  55. devs := []*pluginapi.Device{
  56. {ID: "Dev-1", Health: pluginapi.Healthy},
  57. {ID: "Dev-2", Health: pluginapi.Healthy},
  58. }
  59. socketPath := pluginSockDir + "dp." + fmt.Sprintf("%d", time.Now().Unix())
  60. e2elog.Logf("socketPath %v", socketPath)
  61. dp1 := dm.NewDevicePluginStub(devs, socketPath, resourceName, false)
  62. dp1.SetAllocFunc(stubAllocFunc)
  63. err := dp1.Start()
  64. framework.ExpectNoError(err)
  65. By("Register resources")
  66. err = dp1.Register(pluginapi.KubeletSocket, resourceName, pluginSockDir)
  67. framework.ExpectNoError(err)
  68. By("Waiting for the resource exported by the stub device plugin to become available on the local node")
  69. devsLen := int64(len(devs))
  70. Eventually(func() bool {
  71. node, err := f.ClientSet.CoreV1().Nodes().Get(framework.TestContext.NodeName, metav1.GetOptions{})
  72. framework.ExpectNoError(err)
  73. return numberOfDevicesCapacity(node, resourceName) == devsLen &&
  74. numberOfDevicesAllocatable(node, resourceName) == devsLen
  75. }, 30*time.Second, framework.Poll).Should(BeTrue())
  76. By("Creating one pod on node with at least one fake-device")
  77. podRECMD := "devs=$(ls /tmp/ | egrep '^Dev-[0-9]+$') && echo stub devices: $devs"
  78. pod1 := f.PodClient().CreateSync(makeBusyboxPod(resourceName, podRECMD))
  79. deviceIDRE := "stub devices: (Dev-[0-9]+)"
  80. devId1 := parseLog(f, pod1.Name, pod1.Name, deviceIDRE)
  81. Expect(devId1).To(Not(Equal("")))
  82. podResources, err := getNodeDevices()
  83. Expect(err).To(BeNil())
  84. Expect(len(podResources.PodResources)).To(Equal(1))
  85. Expect(podResources.PodResources[0].Name).To(Equal(pod1.Name))
  86. Expect(podResources.PodResources[0].Namespace).To(Equal(pod1.Namespace))
  87. Expect(len(podResources.PodResources[0].Containers)).To(Equal(1))
  88. Expect(podResources.PodResources[0].Containers[0].Name).To(Equal(pod1.Spec.Containers[0].Name))
  89. Expect(len(podResources.PodResources[0].Containers[0].Devices)).To(Equal(1))
  90. Expect(podResources.PodResources[0].Containers[0].Devices[0].ResourceName).To(Equal(resourceName))
  91. Expect(len(podResources.PodResources[0].Containers[0].Devices[0].DeviceIds)).To(Equal(1))
  92. pod1, err = f.PodClient().Get(pod1.Name, metav1.GetOptions{})
  93. framework.ExpectNoError(err)
  94. ensurePodContainerRestart(f, pod1.Name, pod1.Name)
  95. By("Confirming that device assignment persists even after container restart")
  96. devIdAfterRestart := parseLog(f, pod1.Name, pod1.Name, deviceIDRE)
  97. Expect(devIdAfterRestart).To(Equal(devId1))
  98. restartTime := time.Now()
  99. By("Restarting Kubelet")
  100. restartKubelet()
  101. // We need to wait for node to be ready before re-registering stub device plugin.
  102. // Otherwise, Kubelet DeviceManager may remove the re-registered sockets after it starts.
  103. By("Wait for node is ready")
  104. Eventually(func() bool {
  105. node, err := f.ClientSet.CoreV1().Nodes().Get(framework.TestContext.NodeName, metav1.GetOptions{})
  106. framework.ExpectNoError(err)
  107. for _, cond := range node.Status.Conditions {
  108. if cond.Type == v1.NodeReady && cond.Status == v1.ConditionTrue && cond.LastHeartbeatTime.After(restartTime) {
  109. return true
  110. }
  111. }
  112. return false
  113. }, 5*time.Minute, framework.Poll).Should(BeTrue())
  114. By("Re-Register resources")
  115. dp1 = dm.NewDevicePluginStub(devs, socketPath, resourceName, false)
  116. dp1.SetAllocFunc(stubAllocFunc)
  117. err = dp1.Start()
  118. framework.ExpectNoError(err)
  119. err = dp1.Register(pluginapi.KubeletSocket, resourceName, pluginSockDir)
  120. framework.ExpectNoError(err)
  121. ensurePodContainerRestart(f, pod1.Name, pod1.Name)
  122. By("Confirming that after a kubelet restart, fake-device assignement is kept")
  123. devIdRestart1 := parseLog(f, pod1.Name, pod1.Name, deviceIDRE)
  124. Expect(devIdRestart1).To(Equal(devId1))
  125. By("Waiting for resource to become available on the local node after re-registration")
  126. Eventually(func() bool {
  127. node, err := f.ClientSet.CoreV1().Nodes().Get(framework.TestContext.NodeName, metav1.GetOptions{})
  128. framework.ExpectNoError(err)
  129. return numberOfDevicesCapacity(node, resourceName) == devsLen &&
  130. numberOfDevicesAllocatable(node, resourceName) == devsLen
  131. }, 30*time.Second, framework.Poll).Should(BeTrue())
  132. By("Creating another pod")
  133. pod2 := f.PodClient().CreateSync(makeBusyboxPod(resourceName, podRECMD))
  134. By("Checking that pod got a different fake device")
  135. devId2 := parseLog(f, pod2.Name, pod2.Name, deviceIDRE)
  136. Expect(devId1).To(Not(Equal(devId2)))
  137. By("Deleting device plugin.")
  138. err = dp1.Stop()
  139. framework.ExpectNoError(err)
  140. By("Waiting for stub device plugin to become unhealthy on the local node")
  141. Eventually(func() int64 {
  142. node, err := f.ClientSet.CoreV1().Nodes().Get(framework.TestContext.NodeName, metav1.GetOptions{})
  143. framework.ExpectNoError(err)
  144. return numberOfDevicesAllocatable(node, resourceName)
  145. }, 30*time.Second, framework.Poll).Should(Equal(int64(0)))
  146. By("Checking that scheduled pods can continue to run even after we delete device plugin.")
  147. ensurePodContainerRestart(f, pod1.Name, pod1.Name)
  148. devIdRestart1 = parseLog(f, pod1.Name, pod1.Name, deviceIDRE)
  149. Expect(devIdRestart1).To(Equal(devId1))
  150. ensurePodContainerRestart(f, pod2.Name, pod2.Name)
  151. devIdRestart2 := parseLog(f, pod2.Name, pod2.Name, deviceIDRE)
  152. Expect(devIdRestart2).To(Equal(devId2))
  153. By("Re-register resources")
  154. dp1 = dm.NewDevicePluginStub(devs, socketPath, resourceName, false)
  155. dp1.SetAllocFunc(stubAllocFunc)
  156. err = dp1.Start()
  157. framework.ExpectNoError(err)
  158. err = dp1.Register(pluginapi.KubeletSocket, resourceName, pluginSockDir)
  159. framework.ExpectNoError(err)
  160. By("Waiting for the resource exported by the stub device plugin to become healthy on the local node")
  161. Eventually(func() int64 {
  162. node, err := f.ClientSet.CoreV1().Nodes().Get(framework.TestContext.NodeName, metav1.GetOptions{})
  163. framework.ExpectNoError(err)
  164. return numberOfDevicesAllocatable(node, resourceName)
  165. }, 30*time.Second, framework.Poll).Should(Equal(devsLen))
  166. By("Deleting device plugin again.")
  167. err = dp1.Stop()
  168. framework.ExpectNoError(err)
  169. By("Waiting for stub device plugin to become unavailable on the local node")
  170. Eventually(func() bool {
  171. node, err := f.ClientSet.CoreV1().Nodes().Get(framework.TestContext.NodeName, metav1.GetOptions{})
  172. framework.ExpectNoError(err)
  173. return numberOfDevicesCapacity(node, resourceName) <= 0
  174. }, 10*time.Minute, framework.Poll).Should(BeTrue())
  175. // Cleanup
  176. f.PodClient().DeleteSync(pod1.Name, &metav1.DeleteOptions{}, framework.DefaultPodDeletionTimeout)
  177. f.PodClient().DeleteSync(pod2.Name, &metav1.DeleteOptions{}, framework.DefaultPodDeletionTimeout)
  178. })
  179. })
  180. }
  181. // makeBusyboxPod returns a simple Pod spec with a busybox container
  182. // that requests resourceName and runs the specified command.
  183. func makeBusyboxPod(resourceName, cmd string) *v1.Pod {
  184. podName := "device-plugin-test-" + string(uuid.NewUUID())
  185. rl := v1.ResourceList{v1.ResourceName(resourceName): *resource.NewQuantity(1, resource.DecimalSI)}
  186. return &v1.Pod{
  187. ObjectMeta: metav1.ObjectMeta{Name: podName},
  188. Spec: v1.PodSpec{
  189. RestartPolicy: v1.RestartPolicyAlways,
  190. Containers: []v1.Container{{
  191. Image: busyboxImage,
  192. Name: podName,
  193. // Runs the specified command in the test pod.
  194. Command: []string{"sh", "-c", cmd},
  195. Resources: v1.ResourceRequirements{
  196. Limits: rl,
  197. Requests: rl,
  198. },
  199. }},
  200. },
  201. }
  202. }
  203. // ensurePodContainerRestart confirms that pod container has restarted at least once
  204. func ensurePodContainerRestart(f *framework.Framework, podName string, contName string) {
  205. var initialCount int32
  206. var currentCount int32
  207. p, err := f.PodClient().Get(podName, metav1.GetOptions{})
  208. if err != nil || len(p.Status.ContainerStatuses) < 1 {
  209. framework.Failf("ensurePodContainerRestart failed for pod %q: %v", podName, err)
  210. }
  211. initialCount = p.Status.ContainerStatuses[0].RestartCount
  212. Eventually(func() bool {
  213. p, err = f.PodClient().Get(podName, metav1.GetOptions{})
  214. if err != nil || len(p.Status.ContainerStatuses) < 1 {
  215. return false
  216. }
  217. currentCount = p.Status.ContainerStatuses[0].RestartCount
  218. e2elog.Logf("initial %v, current %v", initialCount, currentCount)
  219. return currentCount > initialCount
  220. }, 5*time.Minute, framework.Poll).Should(BeTrue())
  221. }
  222. // parseLog returns the matching string for the specified regular expression parsed from the container logs.
  223. func parseLog(f *framework.Framework, podName string, contName string, re string) string {
  224. logs, err := framework.GetPodLogs(f.ClientSet, f.Namespace.Name, podName, contName)
  225. if err != nil {
  226. framework.Failf("GetPodLogs for pod %q failed: %v", podName, err)
  227. }
  228. e2elog.Logf("got pod logs: %v", logs)
  229. regex := regexp.MustCompile(re)
  230. matches := regex.FindStringSubmatch(logs)
  231. if len(matches) < 2 {
  232. return ""
  233. }
  234. return matches[1]
  235. }
  236. // numberOfDevicesCapacity returns the number of devices of resourceName advertised by a node capacity
  237. func numberOfDevicesCapacity(node *v1.Node, resourceName string) int64 {
  238. val, ok := node.Status.Capacity[v1.ResourceName(resourceName)]
  239. if !ok {
  240. return 0
  241. }
  242. return val.Value()
  243. }
  244. // numberOfDevicesAllocatable returns the number of devices of resourceName advertised by a node allocatable
  245. func numberOfDevicesAllocatable(node *v1.Node, resourceName string) int64 {
  246. val, ok := node.Status.Allocatable[v1.ResourceName(resourceName)]
  247. if !ok {
  248. return 0
  249. }
  250. return val.Value()
  251. }
  252. // stubAllocFunc will pass to stub device plugin
  253. func stubAllocFunc(r *pluginapi.AllocateRequest, devs map[string]pluginapi.Device) (*pluginapi.AllocateResponse, error) {
  254. var responses pluginapi.AllocateResponse
  255. for _, req := range r.ContainerRequests {
  256. response := &pluginapi.ContainerAllocateResponse{}
  257. for _, requestID := range req.DevicesIDs {
  258. dev, ok := devs[requestID]
  259. if !ok {
  260. return nil, fmt.Errorf("invalid allocation request with non-existing device %s", requestID)
  261. }
  262. if dev.Health != pluginapi.Healthy {
  263. return nil, fmt.Errorf("invalid allocation request with unhealthy device: %s", requestID)
  264. }
  265. // create fake device file
  266. fpath := filepath.Join("/tmp", dev.ID)
  267. // clean first
  268. os.RemoveAll(fpath)
  269. f, err := os.Create(fpath)
  270. if err != nil && !os.IsExist(err) {
  271. return nil, fmt.Errorf("failed to create fake device file: %s", err)
  272. }
  273. f.Close()
  274. response.Mounts = append(response.Mounts, &pluginapi.Mount{
  275. ContainerPath: fpath,
  276. HostPath: fpath,
  277. })
  278. }
  279. responses.ContainerResponses = append(responses.ContainerResponses, response)
  280. }
  281. return &responses, nil
  282. }