1
16
17
21
22 package testsuites
23
24 import (
25 "context"
26 "fmt"
27 "math"
28 "path/filepath"
29 "strconv"
30 "strings"
31 "time"
32
33 "github.com/onsi/ginkgo/v2"
34
35 v1 "k8s.io/api/core/v1"
36 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37 "k8s.io/apimachinery/pkg/util/errors"
38 clientset "k8s.io/client-go/kubernetes"
39 "k8s.io/kubernetes/test/e2e/framework"
40 e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
41 e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
42 e2evolume "k8s.io/kubernetes/test/e2e/framework/volume"
43 storageframework "k8s.io/kubernetes/test/e2e/storage/framework"
44 admissionapi "k8s.io/pod-security-admission/api"
45 )
46
47
48
49
50 var md5hashes = map[int64]string{
51 storageframework.FileSizeSmall: "5c34c2813223a7ca05a3c2f38c0d1710",
52 storageframework.FileSizeMedium: "f2fa202b1ffeedda5f3a58bd1ae81104",
53 storageframework.FileSizeLarge: "8d763edc71bd16217664793b5a15e403",
54 }
55
56 const mountPath = "/opt"
57
58 type volumeIOTestSuite struct {
59 tsInfo storageframework.TestSuiteInfo
60 }
61
62
63
64 func InitCustomVolumeIOTestSuite(patterns []storageframework.TestPattern) storageframework.TestSuite {
65 return &volumeIOTestSuite{
66 tsInfo: storageframework.TestSuiteInfo{
67 Name: "volumeIO",
68 TestPatterns: patterns,
69 SupportedSizeRange: e2evolume.SizeRange{
70 Min: "1Mi",
71 },
72 },
73 }
74 }
75
76
77
78 func InitVolumeIOTestSuite() storageframework.TestSuite {
79 patterns := []storageframework.TestPattern{
80 storageframework.DefaultFsInlineVolume,
81 storageframework.DefaultFsPreprovisionedPV,
82 storageframework.DefaultFsDynamicPV,
83 storageframework.NtfsDynamicPV,
84 }
85 return InitCustomVolumeIOTestSuite(patterns)
86 }
87
88 func (t *volumeIOTestSuite) GetTestSuiteInfo() storageframework.TestSuiteInfo {
89 return t.tsInfo
90 }
91
92 func (t *volumeIOTestSuite) SkipUnsupportedTests(driver storageframework.TestDriver, pattern storageframework.TestPattern) {
93 skipVolTypePatterns(pattern, driver, storageframework.NewVolTypeMap(
94 storageframework.PreprovisionedPV,
95 storageframework.InlineVolume))
96 }
97
98 func (t *volumeIOTestSuite) DefineTests(driver storageframework.TestDriver, pattern storageframework.TestPattern) {
99 type local struct {
100 config *storageframework.PerTestConfig
101
102 resource *storageframework.VolumeResource
103
104 migrationCheck *migrationOpCheck
105 }
106 var (
107 dInfo = driver.GetDriverInfo()
108 l local
109 )
110
111
112
113 f := framework.NewFrameworkWithCustomTimeouts("volumeio", storageframework.GetDriverTimeouts(driver))
114 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
115
116 init := func(ctx context.Context) {
117 l = local{}
118
119
120 l.config = driver.PrepareTest(ctx, f)
121 l.migrationCheck = newMigrationOpCheck(ctx, f.ClientSet, f.ClientConfig(), dInfo.InTreePluginName)
122
123 testVolumeSizeRange := t.GetTestSuiteInfo().SupportedSizeRange
124 l.resource = storageframework.CreateVolumeResource(ctx, driver, l.config, pattern, testVolumeSizeRange)
125 if l.resource.VolSource == nil {
126 e2eskipper.Skipf("Driver %q does not define volumeSource - skipping", dInfo.Name)
127 }
128
129 }
130
131 cleanup := func(ctx context.Context) {
132 var errs []error
133 if l.resource != nil {
134 errs = append(errs, l.resource.CleanupResource(ctx))
135 l.resource = nil
136 }
137
138 framework.ExpectNoError(errors.NewAggregate(errs), "while cleaning up resource")
139 l.migrationCheck.validateMigrationVolumeOpCounts(ctx)
140 }
141
142 f.It("should write files of various sizes, verify size, validate content", f.WithSlow(), func(ctx context.Context) {
143 init(ctx)
144 ginkgo.DeferCleanup(cleanup)
145
146 cs := f.ClientSet
147 fileSizes := createFileSizes(dInfo.MaxFileSize)
148 testFile := fmt.Sprintf("%s_io_test_%s", dInfo.Name, f.Namespace.Name)
149 var fsGroup *int64
150 if !framework.NodeOSDistroIs("windows") && dInfo.Capabilities[storageframework.CapFsGroup] {
151 fsGroupVal := int64(1234)
152 fsGroup = &fsGroupVal
153 }
154 podSec := v1.PodSecurityContext{
155 FSGroup: fsGroup,
156 }
157 err := testVolumeIO(ctx, f, cs, storageframework.ConvertTestConfig(l.config), *l.resource.VolSource, &podSec, testFile, fileSizes)
158 framework.ExpectNoError(err)
159 })
160 }
161
162 func createFileSizes(maxFileSize int64) []int64 {
163 allFileSizes := []int64{
164 storageframework.FileSizeSmall,
165 storageframework.FileSizeMedium,
166 storageframework.FileSizeLarge,
167 }
168 fileSizes := []int64{}
169
170 for _, size := range allFileSizes {
171 if size <= maxFileSize {
172 fileSizes = append(fileSizes, size)
173 }
174 }
175
176 return fileSizes
177 }
178
179
180 func makePodSpec(config e2evolume.TestConfig, initCmd string, volsrc v1.VolumeSource, podSecContext *v1.PodSecurityContext) *v1.Pod {
181 var gracePeriod int64 = 1
182 volName := fmt.Sprintf("io-volume-%s", config.Namespace)
183 pod := &v1.Pod{
184 TypeMeta: metav1.TypeMeta{
185 Kind: "Pod",
186 APIVersion: "v1",
187 },
188 ObjectMeta: metav1.ObjectMeta{
189 Name: config.Prefix + "-io-client",
190 Labels: map[string]string{
191 "role": config.Prefix + "-io-client",
192 },
193 },
194 Spec: v1.PodSpec{
195 InitContainers: []v1.Container{
196 {
197 Name: config.Prefix + "-io-init",
198 Image: e2epod.GetDefaultTestImage(),
199 Command: []string{
200 "/bin/sh",
201 "-c",
202 initCmd,
203 },
204 VolumeMounts: []v1.VolumeMount{
205 {
206 Name: volName,
207 MountPath: mountPath,
208 },
209 },
210 },
211 },
212 Containers: []v1.Container{
213 {
214 Name: config.Prefix + "-io-client",
215 Image: e2epod.GetDefaultTestImage(),
216 Command: []string{
217 "/bin/sh",
218 "-c",
219 "sleep 3600",
220 },
221 VolumeMounts: []v1.VolumeMount{
222 {
223 Name: volName,
224 MountPath: mountPath,
225 },
226 },
227 },
228 },
229 TerminationGracePeriodSeconds: &gracePeriod,
230 SecurityContext: podSecContext,
231 Volumes: []v1.Volume{
232 {
233 Name: volName,
234 VolumeSource: volsrc,
235 },
236 },
237 RestartPolicy: v1.RestartPolicyNever,
238 },
239 }
240
241 e2epod.SetNodeSelection(&pod.Spec, config.ClientNodeSelection)
242 return pod
243 }
244
245
246 func writeToFile(f *framework.Framework, pod *v1.Pod, fpath, ddInput string, fsize int64) error {
247 ginkgo.By(fmt.Sprintf("writing %d bytes to test file %s", fsize, fpath))
248 loopCnt := fsize / storageframework.MinFileSize
249 writeCmd := fmt.Sprintf("i=0; while [ $i -lt %d ]; do dd if=%s bs=%d >>%s 2>/dev/null; let i+=1; done", loopCnt, ddInput, storageframework.MinFileSize, fpath)
250 stdout, stderr, err := e2evolume.PodExec(f, pod, writeCmd)
251 if err != nil {
252 return fmt.Errorf("error writing to volume using %q: %s\nstdout: %s\nstderr: %s", writeCmd, err, stdout, stderr)
253 }
254 return err
255 }
256
257
258 func verifyFile(f *framework.Framework, pod *v1.Pod, fpath string, expectSize int64, ddInput string) error {
259 ginkgo.By("verifying file size")
260 rtnstr, stderr, err := e2evolume.PodExec(f, pod, fmt.Sprintf("stat -c %%s %s", fpath))
261 if err != nil || rtnstr == "" {
262 return fmt.Errorf("unable to get file size via `stat %s`: %v\nstdout: %s\nstderr: %s", fpath, err, rtnstr, stderr)
263 }
264 size, err := strconv.Atoi(strings.TrimSuffix(rtnstr, "\n"))
265 if err != nil {
266 return fmt.Errorf("unable to convert string %q to int: %w", rtnstr, err)
267 }
268 if int64(size) != expectSize {
269 return fmt.Errorf("size of file %s is %d, expected %d", fpath, size, expectSize)
270 }
271
272 ginkgo.By("verifying file hash")
273 rtnstr, stderr, err = e2evolume.PodExec(f, pod, fmt.Sprintf("md5sum %s | cut -d' ' -f1", fpath))
274 if err != nil {
275 return fmt.Errorf("unable to test file hash via `md5sum %s`: %v\nstdout: %s\nstderr: %s", fpath, err, rtnstr, stderr)
276 }
277 actualHash := strings.TrimSuffix(rtnstr, "\n")
278 expectedHash, ok := md5hashes[expectSize]
279 if !ok {
280 return fmt.Errorf("File hash is unknown for file size %d. Was a new file size added to the test suite?",
281 expectSize)
282 }
283 if actualHash != expectedHash {
284 return fmt.Errorf("MD5 hash is incorrect for file %s with size %d. Expected: `%s`; Actual: `%s`",
285 fpath, expectSize, expectedHash, actualHash)
286 }
287
288 return nil
289 }
290
291
292 func deleteFile(f *framework.Framework, pod *v1.Pod, fpath string) {
293 ginkgo.By(fmt.Sprintf("deleting test file %s...", fpath))
294 stdout, stderr, err := e2evolume.PodExec(f, pod, fmt.Sprintf("rm -f %s", fpath))
295 if err != nil {
296
297 framework.Logf("unable to delete test file %s: %v\nerror ignored, continuing test\nstdout: %s\nstderr: %s", fpath, err, stdout, stderr)
298 }
299 }
300
301
302
303
304
305
306
307
308 func testVolumeIO(ctx context.Context, f *framework.Framework, cs clientset.Interface, config e2evolume.TestConfig, volsrc v1.VolumeSource, podSecContext *v1.PodSecurityContext, file string, fsizes []int64) (err error) {
309 ddInput := filepath.Join(mountPath, fmt.Sprintf("%s-%s-dd_if", config.Prefix, config.Namespace))
310 writeBlk := strings.Repeat("abcdefghijklmnopqrstuvwxyz123456", 32)
311 loopCnt := storageframework.MinFileSize / int64(len(writeBlk))
312
313
314
315 initCmd := fmt.Sprintf("i=0; while [ $i -lt %d ]; do echo -n %s >>%s; let i+=1; done", loopCnt, writeBlk, ddInput)
316
317 clientPod := makePodSpec(config, initCmd, volsrc, podSecContext)
318
319 ginkgo.By(fmt.Sprintf("starting %s", clientPod.Name))
320 podsNamespacer := cs.CoreV1().Pods(config.Namespace)
321 clientPod, err = podsNamespacer.Create(ctx, clientPod, metav1.CreateOptions{})
322 if err != nil {
323 return fmt.Errorf("failed to create client pod %q: %w", clientPod.Name, err)
324 }
325 ginkgo.DeferCleanup(func(ctx context.Context) {
326 deleteFile(f, clientPod, ddInput)
327 ginkgo.By(fmt.Sprintf("deleting client pod %q...", clientPod.Name))
328 e := e2epod.DeletePodWithWait(ctx, cs, clientPod)
329 if e != nil {
330 framework.Logf("client pod failed to delete: %v", e)
331 if err == nil {
332 err = e
333 }
334 } else {
335 framework.Logf("sleeping a bit so kubelet can unmount and detach the volume")
336 time.Sleep(e2evolume.PodCleanupTimeout)
337 }
338 })
339
340 err = e2epod.WaitTimeoutForPodRunningInNamespace(ctx, cs, clientPod.Name, clientPod.Namespace, f.Timeouts.PodStart)
341 if err != nil {
342 return fmt.Errorf("client pod %q not running: %w", clientPod.Name, err)
343 }
344
345
346 for _, fsize := range fsizes {
347
348 if math.Mod(float64(fsize), float64(storageframework.MinFileSize)) != 0 {
349 fsize = fsize/storageframework.MinFileSize + storageframework.MinFileSize
350 }
351 fpath := filepath.Join(mountPath, fmt.Sprintf("%s-%d", file, fsize))
352 defer func() {
353 deleteFile(f, clientPod, fpath)
354 }()
355 if err = writeToFile(f, clientPod, fpath, ddInput, fsize); err != nil {
356 return err
357 }
358 if err = verifyFile(f, clientPod, fpath, fsize, ddInput); err != nil {
359 return err
360 }
361 }
362
363 return
364 }
365
View as plain text