ephemeral.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. /*
  2. Copyright 2019 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 testsuites
  14. import (
  15. "context"
  16. "flag"
  17. "fmt"
  18. "strings"
  19. "github.com/onsi/ginkgo"
  20. "github.com/onsi/gomega"
  21. v1 "k8s.io/api/core/v1"
  22. apierrors "k8s.io/apimachinery/pkg/api/errors"
  23. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  24. clientset "k8s.io/client-go/kubernetes"
  25. "k8s.io/kubernetes/test/e2e/framework"
  26. e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
  27. e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
  28. "k8s.io/kubernetes/test/e2e/framework/volume"
  29. "k8s.io/kubernetes/test/e2e/storage/testpatterns"
  30. storageutils "k8s.io/kubernetes/test/e2e/storage/utils"
  31. )
  32. type ephemeralTestSuite struct {
  33. tsInfo TestSuiteInfo
  34. }
  35. var _ TestSuite = &ephemeralTestSuite{}
  36. // InitEphemeralTestSuite returns ephemeralTestSuite that implements TestSuite interface
  37. func InitEphemeralTestSuite() TestSuite {
  38. return &ephemeralTestSuite{
  39. tsInfo: TestSuiteInfo{
  40. Name: "ephemeral",
  41. TestPatterns: []testpatterns.TestPattern{
  42. {
  43. Name: "inline ephemeral CSI volume",
  44. VolType: testpatterns.CSIInlineVolume,
  45. },
  46. },
  47. },
  48. }
  49. }
  50. func (p *ephemeralTestSuite) GetTestSuiteInfo() TestSuiteInfo {
  51. return p.tsInfo
  52. }
  53. func (p *ephemeralTestSuite) SkipRedundantSuite(driver TestDriver, pattern testpatterns.TestPattern) {
  54. }
  55. func (p *ephemeralTestSuite) DefineTests(driver TestDriver, pattern testpatterns.TestPattern) {
  56. type local struct {
  57. config *PerTestConfig
  58. driverCleanup func()
  59. testCase *EphemeralTest
  60. }
  61. var (
  62. dInfo = driver.GetDriverInfo()
  63. eDriver EphemeralTestDriver
  64. l local
  65. )
  66. ginkgo.BeforeEach(func() {
  67. ok := false
  68. eDriver, ok = driver.(EphemeralTestDriver)
  69. if !ok {
  70. e2eskipper.Skipf("Driver %s doesn't support ephemeral inline volumes -- skipping", dInfo.Name)
  71. }
  72. })
  73. // This intentionally comes after checking the preconditions because it
  74. // registers its own BeforeEach which creates the namespace. Beware that it
  75. // also registers an AfterEach which renders f unusable. Any code using
  76. // f must run inside an It or Context callback.
  77. f := framework.NewDefaultFramework("ephemeral")
  78. init := func() {
  79. l = local{}
  80. // Now do the more expensive test initialization.
  81. l.config, l.driverCleanup = driver.PrepareTest(f)
  82. l.testCase = &EphemeralTest{
  83. Client: l.config.Framework.ClientSet,
  84. Namespace: f.Namespace.Name,
  85. DriverName: eDriver.GetCSIDriverName(l.config),
  86. Node: l.config.ClientNodeSelection,
  87. GetVolume: func(volumeNumber int) (map[string]string, bool, bool) {
  88. return eDriver.GetVolume(l.config, volumeNumber)
  89. },
  90. }
  91. }
  92. cleanup := func() {
  93. err := tryFunc(l.driverCleanup)
  94. framework.ExpectNoError(err, "while cleaning up driver")
  95. l.driverCleanup = nil
  96. }
  97. ginkgo.It("should create read-only inline ephemeral volume", func() {
  98. init()
  99. defer cleanup()
  100. l.testCase.ReadOnly = true
  101. l.testCase.RunningPodCheck = func(pod *v1.Pod) interface{} {
  102. storageutils.VerifyExecInPodSucceed(f, pod, "mount | grep /mnt/test | grep ro,")
  103. return nil
  104. }
  105. l.testCase.TestEphemeral()
  106. })
  107. ginkgo.It("should create read/write inline ephemeral volume", func() {
  108. init()
  109. defer cleanup()
  110. l.testCase.ReadOnly = false
  111. l.testCase.RunningPodCheck = func(pod *v1.Pod) interface{} {
  112. storageutils.VerifyExecInPodSucceed(f, pod, "mount | grep /mnt/test | grep rw,")
  113. return nil
  114. }
  115. l.testCase.TestEphemeral()
  116. })
  117. ginkgo.It("should support two pods which share the same volume", func() {
  118. init()
  119. defer cleanup()
  120. // We test in read-only mode if that is all that the driver supports,
  121. // otherwise read/write.
  122. _, shared, readOnly := eDriver.GetVolume(l.config, 0)
  123. l.testCase.RunningPodCheck = func(pod *v1.Pod) interface{} {
  124. // Create another pod with the same inline volume attributes.
  125. pod2 := StartInPodWithInlineVolume(f.ClientSet, f.Namespace.Name, "inline-volume-tester2", "sleep 100000",
  126. []v1.CSIVolumeSource{*pod.Spec.Volumes[0].CSI},
  127. readOnly,
  128. l.testCase.Node)
  129. framework.ExpectNoError(e2epod.WaitForPodRunningInNamespaceSlow(f.ClientSet, pod2.Name, pod2.Namespace), "waiting for second pod with inline volume")
  130. // If (and only if) we were able to mount
  131. // read/write and volume data is not shared
  132. // between pods, then we can check whether
  133. // data written in one pod is really not
  134. // visible in the other.
  135. if !readOnly && !shared {
  136. ginkgo.By("writing data in one pod and checking for it in the second")
  137. storageutils.VerifyExecInPodSucceed(f, pod, "touch /mnt/test-0/hello-world")
  138. storageutils.VerifyExecInPodSucceed(f, pod2, "[ ! -f /mnt/test-0/hello-world ]")
  139. }
  140. defer StopPod(f.ClientSet, pod2)
  141. return nil
  142. }
  143. l.testCase.TestEphemeral()
  144. })
  145. var numInlineVolumes = flag.Int("storage.ephemeral."+strings.Replace(driver.GetDriverInfo().Name, ".", "-", -1)+".numInlineVolumes",
  146. 2, "number of ephemeral inline volumes per pod")
  147. ginkgo.It("should support multiple inline ephemeral volumes", func() {
  148. init()
  149. defer cleanup()
  150. l.testCase.NumInlineVolumes = *numInlineVolumes
  151. gomega.Expect(*numInlineVolumes).To(gomega.BeNumerically(">", 0), "positive number of inline volumes")
  152. l.testCase.TestEphemeral()
  153. })
  154. }
  155. // EphemeralTest represents parameters to be used by tests for inline volumes.
  156. // Not all parameters are used by all tests.
  157. type EphemeralTest struct {
  158. Client clientset.Interface
  159. Namespace string
  160. DriverName string
  161. Node e2epod.NodeSelection
  162. // GetVolume returns the volume attributes for a
  163. // certain inline ephemeral volume, enumerated starting with
  164. // #0. Some tests might require more than one volume. They can
  165. // all be the same or different, depending what the driver supports
  166. // and/or wants to test.
  167. //
  168. // For each volume, the test driver can specify the
  169. // attributes, whether two pods using those attributes will
  170. // end up sharing the same backend storage (i.e. changes made
  171. // in one pod will be visible in the other), and whether
  172. // the volume can be mounted read/write or only read-only.
  173. GetVolume func(volumeNumber int) (attributes map[string]string, shared bool, readOnly bool)
  174. // RunningPodCheck is invoked while a pod using an inline volume is running.
  175. // It can execute additional checks on the pod and its volume(s). Any data
  176. // returned by it is passed to StoppedPodCheck.
  177. RunningPodCheck func(pod *v1.Pod) interface{}
  178. // StoppedPodCheck is invoked after ensuring that the pod is gone.
  179. // It is passed the data gather by RunningPodCheck or nil if that
  180. // isn't defined and then can do additional checks on the node,
  181. // like for example verifying that the ephemeral volume was really
  182. // removed. How to do such a check is driver-specific and not
  183. // covered by the generic storage test suite.
  184. StoppedPodCheck func(nodeName string, runningPodData interface{})
  185. // NumInlineVolumes sets the number of ephemeral inline volumes per pod.
  186. // Unset (= zero) is the same as one.
  187. NumInlineVolumes int
  188. // ReadOnly limits mounting to read-only.
  189. ReadOnly bool
  190. }
  191. // TestEphemeral tests pod creation with one ephemeral volume.
  192. func (t EphemeralTest) TestEphemeral() {
  193. client := t.Client
  194. gomega.Expect(client).NotTo(gomega.BeNil(), "EphemeralTest.Client is required")
  195. gomega.Expect(t.GetVolume).NotTo(gomega.BeNil(), "EphemeralTest.GetVolume is required")
  196. gomega.Expect(t.DriverName).NotTo(gomega.BeEmpty(), "EphemeralTest.DriverName is required")
  197. ginkgo.By(fmt.Sprintf("checking the requested inline volume exists in the pod running on node %+v", t.Node))
  198. command := "mount | grep /mnt/test && sleep 10000"
  199. var csiVolumes []v1.CSIVolumeSource
  200. numVolumes := t.NumInlineVolumes
  201. if numVolumes == 0 {
  202. numVolumes = 1
  203. }
  204. for i := 0; i < numVolumes; i++ {
  205. attributes, _, readOnly := t.GetVolume(i)
  206. csi := v1.CSIVolumeSource{
  207. Driver: t.DriverName,
  208. VolumeAttributes: attributes,
  209. }
  210. if readOnly && !t.ReadOnly {
  211. e2eskipper.Skipf("inline ephemeral volume #%d is read-only, but the test needs a read/write volume", i)
  212. }
  213. csiVolumes = append(csiVolumes, csi)
  214. }
  215. pod := StartInPodWithInlineVolume(client, t.Namespace, "inline-volume-tester", command, csiVolumes, t.ReadOnly, t.Node)
  216. defer func() {
  217. // pod might be nil now.
  218. StopPod(client, pod)
  219. }()
  220. framework.ExpectNoError(e2epod.WaitForPodRunningInNamespaceSlow(client, pod.Name, pod.Namespace), "waiting for pod with inline volume")
  221. runningPod, err := client.CoreV1().Pods(pod.Namespace).Get(context.TODO(), pod.Name, metav1.GetOptions{})
  222. framework.ExpectNoError(err, "get pod")
  223. actualNodeName := runningPod.Spec.NodeName
  224. // Run the checker of the running pod.
  225. var runningPodData interface{}
  226. if t.RunningPodCheck != nil {
  227. runningPodData = t.RunningPodCheck(pod)
  228. }
  229. StopPod(client, pod)
  230. pod = nil // Don't stop twice.
  231. if t.StoppedPodCheck != nil {
  232. t.StoppedPodCheck(actualNodeName, runningPodData)
  233. }
  234. }
  235. // StartInPodWithInlineVolume starts a command in a pod with given volume(s) mounted to /mnt/test-<number> directory.
  236. // The caller is responsible for checking the pod and deleting it.
  237. func StartInPodWithInlineVolume(c clientset.Interface, ns, podName, command string, csiVolumes []v1.CSIVolumeSource, readOnly bool, node e2epod.NodeSelection) *v1.Pod {
  238. pod := &v1.Pod{
  239. TypeMeta: metav1.TypeMeta{
  240. Kind: "Pod",
  241. APIVersion: "v1",
  242. },
  243. ObjectMeta: metav1.ObjectMeta{
  244. GenerateName: podName + "-",
  245. Labels: map[string]string{
  246. "app": podName,
  247. },
  248. },
  249. Spec: v1.PodSpec{
  250. Containers: []v1.Container{
  251. {
  252. Name: "csi-volume-tester",
  253. Image: volume.GetTestImage(framework.BusyBoxImage),
  254. Command: volume.GenerateScriptCmd(command),
  255. },
  256. },
  257. RestartPolicy: v1.RestartPolicyNever,
  258. },
  259. }
  260. e2epod.SetNodeSelection(pod, node)
  261. for i, csiVolume := range csiVolumes {
  262. name := fmt.Sprintf("my-volume-%d", i)
  263. pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts,
  264. v1.VolumeMount{
  265. Name: name,
  266. MountPath: fmt.Sprintf("/mnt/test-%d", i),
  267. ReadOnly: readOnly,
  268. })
  269. pod.Spec.Volumes = append(pod.Spec.Volumes,
  270. v1.Volume{
  271. Name: name,
  272. VolumeSource: v1.VolumeSource{
  273. CSI: &csiVolume,
  274. },
  275. })
  276. }
  277. pod, err := c.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{})
  278. framework.ExpectNoError(err, "failed to create pod")
  279. return pod
  280. }
  281. // CSIInlineVolumesEnabled checks whether the running cluster has the CSIInlineVolumes feature gate enabled.
  282. // It does that by trying to create a pod that uses that feature.
  283. func CSIInlineVolumesEnabled(c clientset.Interface, ns string) (bool, error) {
  284. pod := &v1.Pod{
  285. TypeMeta: metav1.TypeMeta{
  286. Kind: "Pod",
  287. APIVersion: "v1",
  288. },
  289. ObjectMeta: metav1.ObjectMeta{
  290. GenerateName: "csi-inline-volume-",
  291. },
  292. Spec: v1.PodSpec{
  293. Containers: []v1.Container{
  294. {
  295. Name: "csi-volume-tester",
  296. Image: "no-such-registry/no-such-image",
  297. VolumeMounts: []v1.VolumeMount{
  298. {
  299. Name: "my-volume",
  300. MountPath: "/mnt/test",
  301. },
  302. },
  303. },
  304. },
  305. RestartPolicy: v1.RestartPolicyNever,
  306. Volumes: []v1.Volume{
  307. {
  308. Name: "my-volume",
  309. VolumeSource: v1.VolumeSource{
  310. CSI: &v1.CSIVolumeSource{
  311. Driver: "no-such-driver.example.com",
  312. },
  313. },
  314. },
  315. },
  316. },
  317. }
  318. pod, err := c.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{})
  319. switch {
  320. case err == nil:
  321. // Pod was created, feature supported.
  322. StopPod(c, pod)
  323. return true, nil
  324. case apierrors.IsInvalid(err):
  325. // "Invalid" because it uses a feature that isn't supported.
  326. return false, nil
  327. default:
  328. // Unexpected error.
  329. return false, err
  330. }
  331. }