k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/e2e/storage/utils/utils.go (about) 1 /* 2 Copyright 2017 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 utils 18 19 import ( 20 "context" 21 "crypto/sha256" 22 "encoding/base64" 23 "fmt" 24 "math" 25 "math/rand" 26 "path/filepath" 27 "strconv" 28 "strings" 29 "time" 30 31 "github.com/onsi/ginkgo/v2" 32 "github.com/onsi/gomega" 33 34 v1 "k8s.io/api/core/v1" 35 apierrors "k8s.io/apimachinery/pkg/api/errors" 36 "k8s.io/apimachinery/pkg/api/resource" 37 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 38 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 39 "k8s.io/apimachinery/pkg/runtime/schema" 40 "k8s.io/apimachinery/pkg/util/sets" 41 "k8s.io/client-go/dynamic" 42 clientset "k8s.io/client-go/kubernetes" 43 "k8s.io/kubernetes/test/e2e/framework" 44 e2epod "k8s.io/kubernetes/test/e2e/framework/pod" 45 e2essh "k8s.io/kubernetes/test/e2e/framework/ssh" 46 e2evolume "k8s.io/kubernetes/test/e2e/framework/volume" 47 imageutils "k8s.io/kubernetes/test/utils/image" 48 ) 49 50 // KubeletOpt type definition 51 type KubeletOpt string 52 53 const ( 54 // NodeStateTimeout defines Timeout 55 NodeStateTimeout = 1 * time.Minute 56 // KStart defines start value 57 KStart KubeletOpt = "start" 58 // KStop defines stop value 59 KStop KubeletOpt = "stop" 60 // KRestart defines restart value 61 KRestart KubeletOpt = "restart" 62 minValidSize string = "1Ki" 63 maxValidSize string = "10Ei" 64 ) 65 66 // VerifyFSGroupInPod verifies that the passed in filePath contains the expectedFSGroup 67 func VerifyFSGroupInPod(f *framework.Framework, filePath, expectedFSGroup string, pod *v1.Pod) { 68 cmd := fmt.Sprintf("ls -l %s", filePath) 69 stdout, stderr, err := e2evolume.PodExec(f, pod, cmd) 70 framework.ExpectNoError(err) 71 framework.Logf("pod %s/%s exec for cmd %s, stdout: %s, stderr: %s", pod.Namespace, pod.Name, cmd, stdout, stderr) 72 fsGroupResult := strings.Fields(stdout)[3] 73 gomega.Expect(expectedFSGroup).To(gomega.Equal(fsGroupResult), "Expected fsGroup of %s, got %s", expectedFSGroup, fsGroupResult) 74 } 75 76 // getKubeletMainPid return the Main PID of the Kubelet Process 77 func getKubeletMainPid(ctx context.Context, nodeIP string, sudoPresent bool, systemctlPresent bool) string { 78 command := "" 79 if systemctlPresent { 80 command = "systemctl status kubelet | grep 'Main PID'" 81 } else { 82 command = "service kubelet status | grep 'Main PID'" 83 } 84 if sudoPresent { 85 command = fmt.Sprintf("sudo %s", command) 86 } 87 framework.Logf("Attempting `%s`", command) 88 sshResult, err := e2essh.SSH(ctx, command, nodeIP, framework.TestContext.Provider) 89 framework.ExpectNoError(err, fmt.Sprintf("SSH to Node %q errored.", nodeIP)) 90 e2essh.LogResult(sshResult) 91 gomega.Expect(sshResult.Code).To(gomega.BeZero(), "Failed to get kubelet PID") 92 gomega.Expect(sshResult.Stdout).NotTo(gomega.BeEmpty(), "Kubelet Main PID should not be Empty") 93 return sshResult.Stdout 94 } 95 96 // TestKubeletRestartsAndRestoresMount tests that a volume mounted to a pod remains mounted after a kubelet restarts 97 func TestKubeletRestartsAndRestoresMount(ctx context.Context, c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, volumePath string) { 98 byteLen := 64 99 seed := time.Now().UTC().UnixNano() 100 101 ginkgo.By("Writing to the volume.") 102 CheckWriteToPath(f, clientPod, v1.PersistentVolumeFilesystem, false, volumePath, byteLen, seed) 103 104 ginkgo.By("Restarting kubelet") 105 KubeletCommand(ctx, KRestart, c, clientPod) 106 107 ginkgo.By("Testing that written file is accessible.") 108 CheckReadFromPath(f, clientPod, v1.PersistentVolumeFilesystem, false, volumePath, byteLen, seed) 109 110 framework.Logf("Volume mount detected on pod %s and written file %s is readable post-restart.", clientPod.Name, volumePath) 111 } 112 113 // TestKubeletRestartsAndRestoresMap tests that a volume mapped to a pod remains mapped after a kubelet restarts 114 func TestKubeletRestartsAndRestoresMap(ctx context.Context, c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, volumePath string) { 115 byteLen := 64 116 seed := time.Now().UTC().UnixNano() 117 118 ginkgo.By("Writing to the volume.") 119 CheckWriteToPath(f, clientPod, v1.PersistentVolumeBlock, false, volumePath, byteLen, seed) 120 121 ginkgo.By("Restarting kubelet") 122 KubeletCommand(ctx, KRestart, c, clientPod) 123 124 ginkgo.By("Testing that written pv is accessible.") 125 CheckReadFromPath(f, clientPod, v1.PersistentVolumeBlock, false, volumePath, byteLen, seed) 126 127 framework.Logf("Volume map detected on pod %s and written data %s is readable post-restart.", clientPod.Name, volumePath) 128 } 129 130 // TestVolumeUnmountsFromDeletedPodWithForceOption tests that a volume unmounts if the client pod was deleted while the kubelet was down. 131 // forceDelete is true indicating whether the pod is forcefully deleted. 132 // checkSubpath is true indicating whether the subpath should be checked. 133 // If secondPod is set, it is started when kubelet is down to check that the volume is usable while the old pod is being deleted and the new pod is starting. 134 func TestVolumeUnmountsFromDeletedPodWithForceOption(ctx context.Context, c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, forceDelete bool, checkSubpath bool, secondPod *v1.Pod, volumePath string) { 135 nodeIP, err := getHostAddress(ctx, c, clientPod) 136 framework.ExpectNoError(err) 137 nodeIP = nodeIP + ":22" 138 139 ginkgo.By("Expecting the volume mount to be found.") 140 result, err := e2essh.SSH(ctx, fmt.Sprintf("mount | grep %s | grep -v volume-subpaths", clientPod.UID), nodeIP, framework.TestContext.Provider) 141 e2essh.LogResult(result) 142 framework.ExpectNoError(err, "Encountered SSH error.") 143 gomega.Expect(result.Code).To(gomega.Equal(0), fmt.Sprintf("Expected grep exit code of 0, got %d", result.Code)) 144 145 if checkSubpath { 146 ginkgo.By("Expecting the volume subpath mount to be found.") 147 result, err := e2essh.SSH(ctx, fmt.Sprintf("cat /proc/self/mountinfo | grep %s | grep volume-subpaths", clientPod.UID), nodeIP, framework.TestContext.Provider) 148 e2essh.LogResult(result) 149 framework.ExpectNoError(err, "Encountered SSH error.") 150 gomega.Expect(result.Code).To(gomega.Equal(0), fmt.Sprintf("Expected grep exit code of 0, got %d", result.Code)) 151 } 152 153 ginkgo.By("Writing to the volume.") 154 byteLen := 64 155 seed := time.Now().UTC().UnixNano() 156 CheckWriteToPath(f, clientPod, v1.PersistentVolumeFilesystem, false, volumePath, byteLen, seed) 157 158 // This command is to make sure kubelet is started after test finishes no matter it fails or not. 159 ginkgo.DeferCleanup(KubeletCommand, KStart, c, clientPod) 160 ginkgo.By("Stopping the kubelet.") 161 KubeletCommand(ctx, KStop, c, clientPod) 162 163 if secondPod != nil { 164 ginkgo.By("Starting the second pod") 165 _, err = c.CoreV1().Pods(clientPod.Namespace).Create(context.TODO(), secondPod, metav1.CreateOptions{}) 166 framework.ExpectNoError(err, "when starting the second pod") 167 } 168 169 ginkgo.By(fmt.Sprintf("Deleting Pod %q", clientPod.Name)) 170 if forceDelete { 171 err = c.CoreV1().Pods(clientPod.Namespace).Delete(ctx, clientPod.Name, *metav1.NewDeleteOptions(0)) 172 } else { 173 err = c.CoreV1().Pods(clientPod.Namespace).Delete(ctx, clientPod.Name, metav1.DeleteOptions{}) 174 } 175 framework.ExpectNoError(err) 176 177 ginkgo.By("Starting the kubelet and waiting for pod to delete.") 178 KubeletCommand(ctx, KStart, c, clientPod) 179 err = e2epod.WaitForPodNotFoundInNamespace(ctx, f.ClientSet, clientPod.Name, f.Namespace.Name, f.Timeouts.PodDelete) 180 if err != nil { 181 framework.ExpectNoError(err, "Expected pod to be not found.") 182 } 183 184 if forceDelete { 185 // With forceDelete, since pods are immediately deleted from API server, there is no way to be sure when volumes are torn down 186 // so wait some time to finish 187 time.Sleep(30 * time.Second) 188 } 189 190 if secondPod != nil { 191 ginkgo.By("Waiting for the second pod.") 192 err = e2epod.WaitForPodRunningInNamespace(ctx, c, secondPod) 193 framework.ExpectNoError(err, "while waiting for the second pod Running") 194 195 ginkgo.By("Getting the second pod uuid.") 196 secondPod, err := c.CoreV1().Pods(secondPod.Namespace).Get(context.TODO(), secondPod.Name, metav1.GetOptions{}) 197 framework.ExpectNoError(err, "getting the second UID") 198 199 ginkgo.By("Expecting the volume mount to be found in the second pod.") 200 result, err := e2essh.SSH(ctx, fmt.Sprintf("mount | grep %s | grep -v volume-subpaths", secondPod.UID), nodeIP, framework.TestContext.Provider) 201 e2essh.LogResult(result) 202 framework.ExpectNoError(err, "Encountered SSH error when checking the second pod.") 203 gomega.Expect(result.Code).To(gomega.Equal(0), fmt.Sprintf("Expected grep exit code of 0, got %d", result.Code)) 204 205 ginkgo.By("Testing that written file is accessible in the second pod.") 206 CheckReadFromPath(f, secondPod, v1.PersistentVolumeFilesystem, false, volumePath, byteLen, seed) 207 err = c.CoreV1().Pods(secondPod.Namespace).Delete(context.TODO(), secondPod.Name, metav1.DeleteOptions{}) 208 framework.ExpectNoError(err, "when deleting the second pod") 209 err = e2epod.WaitForPodNotFoundInNamespace(ctx, f.ClientSet, secondPod.Name, f.Namespace.Name, f.Timeouts.PodDelete) 210 framework.ExpectNoError(err, "when waiting for the second pod to disappear") 211 } 212 213 ginkgo.By("Expecting the volume mount not to be found.") 214 result, err = e2essh.SSH(ctx, fmt.Sprintf("mount | grep %s | grep -v volume-subpaths", clientPod.UID), nodeIP, framework.TestContext.Provider) 215 e2essh.LogResult(result) 216 framework.ExpectNoError(err, "Encountered SSH error.") 217 gomega.Expect(result.Stdout).To(gomega.BeEmpty(), "Expected grep stdout to be empty (i.e. no mount found).") 218 framework.Logf("Volume unmounted on node %s", clientPod.Spec.NodeName) 219 220 if checkSubpath { 221 ginkgo.By("Expecting the volume subpath mount not to be found.") 222 result, err = e2essh.SSH(ctx, fmt.Sprintf("cat /proc/self/mountinfo | grep %s | grep volume-subpaths", clientPod.UID), nodeIP, framework.TestContext.Provider) 223 e2essh.LogResult(result) 224 framework.ExpectNoError(err, "Encountered SSH error.") 225 gomega.Expect(result.Stdout).To(gomega.BeEmpty(), "Expected grep stdout to be empty (i.e. no subpath mount found).") 226 framework.Logf("Subpath volume unmounted on node %s", clientPod.Spec.NodeName) 227 } 228 229 } 230 231 // TestVolumeUnmountsFromDeletedPod tests that a volume unmounts if the client pod was deleted while the kubelet was down. 232 func TestVolumeUnmountsFromDeletedPod(ctx context.Context, c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, volumePath string) { 233 TestVolumeUnmountsFromDeletedPodWithForceOption(ctx, c, f, clientPod, false, false, nil, volumePath) 234 } 235 236 // TestVolumeUnmountsFromForceDeletedPod tests that a volume unmounts if the client pod was forcefully deleted while the kubelet was down. 237 func TestVolumeUnmountsFromForceDeletedPod(ctx context.Context, c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, volumePath string) { 238 TestVolumeUnmountsFromDeletedPodWithForceOption(ctx, c, f, clientPod, true, false, nil, volumePath) 239 } 240 241 // TestVolumeUnmapsFromDeletedPodWithForceOption tests that a volume unmaps if the client pod was deleted while the kubelet was down. 242 // forceDelete is true indicating whether the pod is forcefully deleted. 243 func TestVolumeUnmapsFromDeletedPodWithForceOption(ctx context.Context, c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, forceDelete bool, devicePath string) { 244 nodeIP, err := getHostAddress(ctx, c, clientPod) 245 framework.ExpectNoError(err, "Failed to get nodeIP.") 246 nodeIP = nodeIP + ":22" 247 248 // Creating command to check whether path exists 249 podDirectoryCmd := fmt.Sprintf("ls /var/lib/kubelet/pods/%s/volumeDevices/*/ | grep '.'", clientPod.UID) 250 if isSudoPresent(ctx, nodeIP, framework.TestContext.Provider) { 251 podDirectoryCmd = fmt.Sprintf("sudo sh -c \"%s\"", podDirectoryCmd) 252 } 253 // Directories in the global directory have unpredictable names, however, device symlinks 254 // have the same name as pod.UID. So just find anything with pod.UID name. 255 globalBlockDirectoryCmd := fmt.Sprintf("find /var/lib/kubelet/plugins -name %s", clientPod.UID) 256 if isSudoPresent(ctx, nodeIP, framework.TestContext.Provider) { 257 globalBlockDirectoryCmd = fmt.Sprintf("sudo sh -c \"%s\"", globalBlockDirectoryCmd) 258 } 259 260 ginkgo.By("Expecting the symlinks from PodDeviceMapPath to be found.") 261 result, err := e2essh.SSH(ctx, podDirectoryCmd, nodeIP, framework.TestContext.Provider) 262 e2essh.LogResult(result) 263 framework.ExpectNoError(err, "Encountered SSH error.") 264 gomega.Expect(result.Code).To(gomega.Equal(0), fmt.Sprintf("Expected grep exit code of 0, got %d", result.Code)) 265 266 ginkgo.By("Expecting the symlinks from global map path to be found.") 267 result, err = e2essh.SSH(ctx, globalBlockDirectoryCmd, nodeIP, framework.TestContext.Provider) 268 e2essh.LogResult(result) 269 framework.ExpectNoError(err, "Encountered SSH error.") 270 gomega.Expect(result.Code).To(gomega.Equal(0), fmt.Sprintf("Expected find exit code of 0, got %d", result.Code)) 271 272 // This command is to make sure kubelet is started after test finishes no matter it fails or not. 273 ginkgo.DeferCleanup(KubeletCommand, KStart, c, clientPod) 274 ginkgo.By("Stopping the kubelet.") 275 KubeletCommand(ctx, KStop, c, clientPod) 276 277 ginkgo.By(fmt.Sprintf("Deleting Pod %q", clientPod.Name)) 278 if forceDelete { 279 err = c.CoreV1().Pods(clientPod.Namespace).Delete(ctx, clientPod.Name, *metav1.NewDeleteOptions(0)) 280 } else { 281 err = c.CoreV1().Pods(clientPod.Namespace).Delete(ctx, clientPod.Name, metav1.DeleteOptions{}) 282 } 283 framework.ExpectNoError(err, "Failed to delete pod.") 284 285 ginkgo.By("Starting the kubelet and waiting for pod to delete.") 286 KubeletCommand(ctx, KStart, c, clientPod) 287 err = e2epod.WaitForPodNotFoundInNamespace(ctx, f.ClientSet, clientPod.Name, f.Namespace.Name, f.Timeouts.PodDelete) 288 framework.ExpectNoError(err, "Expected pod to be not found.") 289 290 if forceDelete { 291 // With forceDelete, since pods are immediately deleted from API server, there is no way to be sure when volumes are torn down 292 // so wait some time to finish 293 time.Sleep(30 * time.Second) 294 } 295 296 ginkgo.By("Expecting the symlink from PodDeviceMapPath not to be found.") 297 result, err = e2essh.SSH(ctx, podDirectoryCmd, nodeIP, framework.TestContext.Provider) 298 e2essh.LogResult(result) 299 framework.ExpectNoError(err, "Encountered SSH error.") 300 gomega.Expect(result.Stdout).To(gomega.BeEmpty(), "Expected grep stdout to be empty.") 301 302 ginkgo.By("Expecting the symlinks from global map path not to be found.") 303 result, err = e2essh.SSH(ctx, globalBlockDirectoryCmd, nodeIP, framework.TestContext.Provider) 304 e2essh.LogResult(result) 305 framework.ExpectNoError(err, "Encountered SSH error.") 306 gomega.Expect(result.Stdout).To(gomega.BeEmpty(), "Expected find stdout to be empty.") 307 308 framework.Logf("Volume unmaped on node %s", clientPod.Spec.NodeName) 309 } 310 311 // TestVolumeUnmapsFromDeletedPod tests that a volume unmaps if the client pod was deleted while the kubelet was down. 312 func TestVolumeUnmapsFromDeletedPod(ctx context.Context, c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, devicePath string) { 313 TestVolumeUnmapsFromDeletedPodWithForceOption(ctx, c, f, clientPod, false, devicePath) 314 } 315 316 // TestVolumeUnmapsFromForceDeletedPod tests that a volume unmaps if the client pod was forcefully deleted while the kubelet was down. 317 func TestVolumeUnmapsFromForceDeletedPod(ctx context.Context, c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, devicePath string) { 318 TestVolumeUnmapsFromDeletedPodWithForceOption(ctx, c, f, clientPod, true, devicePath) 319 } 320 321 // RunInPodWithVolume runs a command in a pod with given claim mounted to /mnt directory. 322 func RunInPodWithVolume(ctx context.Context, c clientset.Interface, t *framework.TimeoutContext, ns, claimName, command string) { 323 pod := &v1.Pod{ 324 TypeMeta: metav1.TypeMeta{ 325 Kind: "Pod", 326 APIVersion: "v1", 327 }, 328 ObjectMeta: metav1.ObjectMeta{ 329 GenerateName: "pvc-volume-tester-", 330 }, 331 Spec: v1.PodSpec{ 332 Containers: []v1.Container{ 333 { 334 Name: "volume-tester", 335 Image: imageutils.GetE2EImage(imageutils.BusyBox), 336 Command: []string{"/bin/sh"}, 337 Args: []string{"-c", command}, 338 VolumeMounts: []v1.VolumeMount{ 339 { 340 Name: "my-volume", 341 MountPath: "/mnt/test", 342 }, 343 }, 344 }, 345 }, 346 RestartPolicy: v1.RestartPolicyNever, 347 Volumes: []v1.Volume{ 348 { 349 Name: "my-volume", 350 VolumeSource: v1.VolumeSource{ 351 PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ 352 ClaimName: claimName, 353 ReadOnly: false, 354 }, 355 }, 356 }, 357 }, 358 }, 359 } 360 pod, err := c.CoreV1().Pods(ns).Create(ctx, pod, metav1.CreateOptions{}) 361 framework.ExpectNoError(err, "Failed to create pod: %v", err) 362 ginkgo.DeferCleanup(e2epod.DeletePodOrFail, c, ns, pod.Name) 363 framework.ExpectNoError(e2epod.WaitForPodSuccessInNamespaceTimeout(ctx, c, pod.Name, pod.Namespace, t.PodStartSlow)) 364 } 365 366 // StartExternalProvisioner create external provisioner pod 367 func StartExternalProvisioner(ctx context.Context, c clientset.Interface, ns string, externalPluginName string) *v1.Pod { 368 podClient := c.CoreV1().Pods(ns) 369 370 provisionerPod := &v1.Pod{ 371 TypeMeta: metav1.TypeMeta{ 372 Kind: "Pod", 373 APIVersion: "v1", 374 }, 375 ObjectMeta: metav1.ObjectMeta{ 376 GenerateName: "external-provisioner-", 377 }, 378 379 Spec: v1.PodSpec{ 380 Containers: []v1.Container{ 381 { 382 Name: "nfs-provisioner", 383 Image: imageutils.GetE2EImage(imageutils.NFSProvisioner), 384 SecurityContext: &v1.SecurityContext{ 385 Capabilities: &v1.Capabilities{ 386 Add: []v1.Capability{"DAC_READ_SEARCH"}, 387 }, 388 }, 389 Args: []string{ 390 "-provisioner=" + externalPluginName, 391 "-grace-period=0", 392 }, 393 Ports: []v1.ContainerPort{ 394 {Name: "nfs", ContainerPort: 2049}, 395 {Name: "mountd", ContainerPort: 20048}, 396 {Name: "rpcbind", ContainerPort: 111}, 397 {Name: "rpcbind-udp", ContainerPort: 111, Protocol: v1.ProtocolUDP}, 398 }, 399 Env: []v1.EnvVar{ 400 { 401 Name: "POD_IP", 402 ValueFrom: &v1.EnvVarSource{ 403 FieldRef: &v1.ObjectFieldSelector{ 404 FieldPath: "status.podIP", 405 }, 406 }, 407 }, 408 }, 409 ImagePullPolicy: v1.PullIfNotPresent, 410 VolumeMounts: []v1.VolumeMount{ 411 { 412 Name: "export-volume", 413 MountPath: "/export", 414 }, 415 }, 416 }, 417 }, 418 Volumes: []v1.Volume{ 419 { 420 Name: "export-volume", 421 VolumeSource: v1.VolumeSource{ 422 EmptyDir: &v1.EmptyDirVolumeSource{}, 423 }, 424 }, 425 }, 426 }, 427 } 428 provisionerPod, err := podClient.Create(ctx, provisionerPod, metav1.CreateOptions{}) 429 framework.ExpectNoError(err, "Failed to create %s pod: %v", provisionerPod.Name, err) 430 431 framework.ExpectNoError(e2epod.WaitForPodRunningInNamespace(ctx, c, provisionerPod)) 432 433 ginkgo.By("locating the provisioner pod") 434 pod, err := podClient.Get(ctx, provisionerPod.Name, metav1.GetOptions{}) 435 framework.ExpectNoError(err, "Cannot locate the provisioner pod %v: %v", provisionerPod.Name, err) 436 437 return pod 438 } 439 440 func isSudoPresent(ctx context.Context, nodeIP string, provider string) bool { 441 framework.Logf("Checking if sudo command is present") 442 sshResult, err := e2essh.SSH(ctx, "sudo --version", nodeIP, provider) 443 framework.ExpectNoError(err, "SSH to %q errored.", nodeIP) 444 if !strings.Contains(sshResult.Stderr, "command not found") { 445 return true 446 } 447 return false 448 } 449 450 // CheckReadWriteToPath check that path can b e read and written 451 func CheckReadWriteToPath(f *framework.Framework, pod *v1.Pod, volMode v1.PersistentVolumeMode, path string) { 452 if volMode == v1.PersistentVolumeBlock { 453 // random -> file1 454 e2evolume.VerifyExecInPodSucceed(f, pod, "dd if=/dev/urandom of=/tmp/file1 bs=64 count=1") 455 // file1 -> dev (write to dev) 456 e2evolume.VerifyExecInPodSucceed(f, pod, fmt.Sprintf("dd if=/tmp/file1 of=%s bs=64 count=1", path)) 457 // dev -> file2 (read from dev) 458 e2evolume.VerifyExecInPodSucceed(f, pod, fmt.Sprintf("dd if=%s of=/tmp/file2 bs=64 count=1", path)) 459 // file1 == file2 (check contents) 460 e2evolume.VerifyExecInPodSucceed(f, pod, "diff /tmp/file1 /tmp/file2") 461 // Clean up temp files 462 e2evolume.VerifyExecInPodSucceed(f, pod, "rm -f /tmp/file1 /tmp/file2") 463 464 // Check that writing file to block volume fails 465 e2evolume.VerifyExecInPodFail(f, pod, fmt.Sprintf("echo 'Hello world.' > %s/file1.txt", path), 1) 466 } else { 467 // text -> file1 (write to file) 468 e2evolume.VerifyExecInPodSucceed(f, pod, fmt.Sprintf("echo 'Hello world.' > %s/file1.txt", path)) 469 // grep file1 (read from file and check contents) 470 e2evolume.VerifyExecInPodSucceed(f, pod, readFile("Hello word.", path)) 471 // Check that writing to directory as block volume fails 472 e2evolume.VerifyExecInPodFail(f, pod, fmt.Sprintf("dd if=/dev/urandom of=%s bs=64 count=1", path), 1) 473 } 474 } 475 476 func readFile(content, path string) string { 477 if framework.NodeOSDistroIs("windows") { 478 return fmt.Sprintf("Select-String '%s' %s/file1.txt", content, path) 479 } 480 return fmt.Sprintf("grep 'Hello world.' %s/file1.txt", path) 481 } 482 483 // genBinDataFromSeed generate binData with random seed 484 func genBinDataFromSeed(len int, seed int64) []byte { 485 binData := make([]byte, len) 486 rand.Seed(seed) 487 488 _, err := rand.Read(binData) 489 if err != nil { 490 fmt.Printf("Error: %v\n", err) 491 } 492 493 return binData 494 } 495 496 // CheckReadFromPath validate that file can be properly read. 497 // 498 // Note: directIO does not work with (default) BusyBox Pods. A requirement for 499 // directIO to function correctly, is to read whole sector(s) for Block-mode 500 // PVCs (normally a sector is 512 bytes), or memory pages for files (commonly 501 // 4096 bytes). 502 func CheckReadFromPath(f *framework.Framework, pod *v1.Pod, volMode v1.PersistentVolumeMode, directIO bool, path string, len int, seed int64) { 503 var pathForVolMode string 504 var iflag string 505 506 if volMode == v1.PersistentVolumeBlock { 507 pathForVolMode = path 508 } else { 509 pathForVolMode = filepath.Join(path, "file1.txt") 510 } 511 512 if directIO { 513 iflag = "iflag=direct" 514 } 515 516 sum := sha256.Sum256(genBinDataFromSeed(len, seed)) 517 518 e2evolume.VerifyExecInPodSucceed(f, pod, fmt.Sprintf("dd if=%s %s bs=%d count=1 | sha256sum", pathForVolMode, iflag, len)) 519 e2evolume.VerifyExecInPodSucceed(f, pod, fmt.Sprintf("dd if=%s %s bs=%d count=1 | sha256sum | grep -Fq %x", pathForVolMode, iflag, len, sum)) 520 } 521 522 // CheckWriteToPath that file can be properly written. 523 // 524 // Note: nocache does not work with (default) BusyBox Pods. To read without 525 // caching, enable directIO with CheckReadFromPath and check the hints about 526 // the len requirements. 527 func CheckWriteToPath(f *framework.Framework, pod *v1.Pod, volMode v1.PersistentVolumeMode, nocache bool, path string, len int, seed int64) { 528 var pathForVolMode string 529 var oflag string 530 531 if volMode == v1.PersistentVolumeBlock { 532 pathForVolMode = path 533 } else { 534 pathForVolMode = filepath.Join(path, "file1.txt") 535 } 536 537 if nocache { 538 oflag = "oflag=nocache" 539 } 540 541 encoded := base64.StdEncoding.EncodeToString(genBinDataFromSeed(len, seed)) 542 543 e2evolume.VerifyExecInPodSucceed(f, pod, fmt.Sprintf("echo %s | base64 -d | sha256sum", encoded)) 544 e2evolume.VerifyExecInPodSucceed(f, pod, fmt.Sprintf("echo %s | base64 -d | dd of=%s %s bs=%d count=1", encoded, pathForVolMode, oflag, len)) 545 } 546 547 // GetSectorSize returns the sector size of the device. 548 func GetSectorSize(f *framework.Framework, pod *v1.Pod, device string) int { 549 stdout, _, err := e2evolume.PodExec(f, pod, fmt.Sprintf("blockdev --getss %s", device)) 550 framework.ExpectNoError(err, "Failed to get sector size of %s", device) 551 ss, err := strconv.Atoi(stdout) 552 framework.ExpectNoError(err, "Sector size returned by blockdev command isn't integer value.") 553 554 return ss 555 } 556 557 // findMountPoints returns all mount points on given node under specified directory. 558 func findMountPoints(ctx context.Context, hostExec HostExec, node *v1.Node, dir string) []string { 559 result, err := hostExec.IssueCommandWithResult(ctx, fmt.Sprintf(`find %s -type d -exec mountpoint {} \; | grep 'is a mountpoint$' || true`, dir), node) 560 framework.ExpectNoError(err, "Encountered HostExec error.") 561 var mountPoints []string 562 if err != nil { 563 for _, line := range strings.Split(result, "\n") { 564 if line == "" { 565 continue 566 } 567 mountPoints = append(mountPoints, strings.TrimSuffix(line, " is a mountpoint")) 568 } 569 } 570 return mountPoints 571 } 572 573 // FindVolumeGlobalMountPoints returns all volume global mount points on the node of given pod. 574 func FindVolumeGlobalMountPoints(ctx context.Context, hostExec HostExec, node *v1.Node) sets.String { 575 return sets.NewString(findMountPoints(ctx, hostExec, node, "/var/lib/kubelet/plugins")...) 576 } 577 578 // CreateDriverNamespace creates a namespace for CSI driver installation. 579 // The namespace is still tracked and ensured that gets deleted when test terminates. 580 func CreateDriverNamespace(ctx context.Context, f *framework.Framework) *v1.Namespace { 581 ginkgo.By(fmt.Sprintf("Building a driver namespace object, basename %s", f.Namespace.Name)) 582 // The driver namespace will be bound to the test namespace in the prefix 583 namespace, err := f.CreateNamespace(ctx, f.Namespace.Name, map[string]string{ 584 "e2e-framework": f.BaseName, 585 "e2e-test-namespace": f.Namespace.Name, 586 }) 587 framework.ExpectNoError(err) 588 589 if framework.TestContext.VerifyServiceAccount { 590 ginkgo.By("Waiting for a default service account to be provisioned in namespace") 591 err = framework.WaitForDefaultServiceAccountInNamespace(ctx, f.ClientSet, namespace.Name) 592 framework.ExpectNoError(err) 593 } else { 594 framework.Logf("Skipping waiting for service account") 595 } 596 return namespace 597 } 598 599 // WaitForGVRDeletion waits until a non-namespaced object has been deleted 600 func WaitForGVRDeletion(ctx context.Context, c dynamic.Interface, gvr schema.GroupVersionResource, objectName string, poll, timeout time.Duration) error { 601 framework.Logf("Waiting up to %v for %s %s to be deleted", timeout, gvr.Resource, objectName) 602 603 if successful := WaitUntil(poll, timeout, func() bool { 604 _, err := c.Resource(gvr).Get(ctx, objectName, metav1.GetOptions{}) 605 if err != nil && apierrors.IsNotFound(err) { 606 framework.Logf("%s %v is not found and has been deleted", gvr.Resource, objectName) 607 return true 608 } else if err != nil { 609 framework.Logf("Get %s returned an error: %v", objectName, err.Error()) 610 } else { 611 framework.Logf("%s %v has been found and is not deleted", gvr.Resource, objectName) 612 } 613 614 return false 615 }); successful { 616 return nil 617 } 618 619 return fmt.Errorf("%s %s is not deleted within %v", gvr.Resource, objectName, timeout) 620 } 621 622 // EnsureGVRDeletion checks that no object as defined by the group/version/kind and name is ever found during the given time period 623 func EnsureGVRDeletion(ctx context.Context, c dynamic.Interface, gvr schema.GroupVersionResource, objectName string, poll, timeout time.Duration, namespace string) error { 624 var resourceClient dynamic.ResourceInterface 625 if namespace != "" { 626 resourceClient = c.Resource(gvr).Namespace(namespace) 627 } else { 628 resourceClient = c.Resource(gvr) 629 } 630 631 err := framework.Gomega().Eventually(ctx, func(ctx context.Context) error { 632 _, err := resourceClient.Get(ctx, objectName, metav1.GetOptions{}) 633 return err 634 }).WithTimeout(timeout).WithPolling(poll).Should(gomega.MatchError(apierrors.IsNotFound, fmt.Sprintf("failed to delete %s %s", gvr, objectName))) 635 return err 636 } 637 638 // EnsureNoGVRDeletion checks that an object as defined by the group/version/kind and name has not been deleted during the given time period 639 func EnsureNoGVRDeletion(ctx context.Context, c dynamic.Interface, gvr schema.GroupVersionResource, objectName string, poll, timeout time.Duration, namespace string) error { 640 var resourceClient dynamic.ResourceInterface 641 if namespace != "" { 642 resourceClient = c.Resource(gvr).Namespace(namespace) 643 } else { 644 resourceClient = c.Resource(gvr) 645 } 646 err := framework.Gomega().Consistently(ctx, func(ctx context.Context) error { 647 _, err := resourceClient.Get(ctx, objectName, metav1.GetOptions{}) 648 if err != nil { 649 return fmt.Errorf("failed to get %s %s: %w", gvr.Resource, objectName, err) 650 } 651 return nil 652 }).WithTimeout(timeout).WithPolling(poll).Should(gomega.Succeed()) 653 return err 654 } 655 656 // WaitForNamespacedGVRDeletion waits until a namespaced object has been deleted 657 func WaitForNamespacedGVRDeletion(ctx context.Context, c dynamic.Interface, gvr schema.GroupVersionResource, ns, objectName string, poll, timeout time.Duration) error { 658 framework.Logf("Waiting up to %v for %s %s to be deleted", timeout, gvr.Resource, objectName) 659 660 if successful := WaitUntil(poll, timeout, func() bool { 661 _, err := c.Resource(gvr).Namespace(ns).Get(ctx, objectName, metav1.GetOptions{}) 662 if err != nil && apierrors.IsNotFound(err) { 663 framework.Logf("%s %s is not found in namespace %s and has been deleted", gvr.Resource, objectName, ns) 664 return true 665 } else if err != nil { 666 framework.Logf("Get %s in namespace %s returned an error: %v", objectName, ns, err.Error()) 667 } else { 668 framework.Logf("%s %s has been found in namespace %s and is not deleted", gvr.Resource, objectName, ns) 669 } 670 671 return false 672 }); successful { 673 return nil 674 } 675 676 return fmt.Errorf("%s %s in namespace %s is not deleted within %v", gvr.Resource, objectName, ns, timeout) 677 } 678 679 // WaitUntil runs checkDone until a timeout is reached 680 func WaitUntil(poll, timeout time.Duration, checkDone func() bool) bool { 681 // TODO (pohly): replace with gomega.Eventually 682 for start := time.Now(); time.Since(start) < timeout; time.Sleep(poll) { 683 if checkDone() { 684 framework.Logf("WaitUntil finished successfully after %v", time.Since(start)) 685 return true 686 } 687 } 688 689 framework.Logf("WaitUntil failed after reaching the timeout %v", timeout) 690 return false 691 } 692 693 // WaitForGVRFinalizer waits until a object from a given GVR contains a finalizer 694 // If namespace is empty, assume it is a non-namespaced object 695 func WaitForGVRFinalizer(ctx context.Context, c dynamic.Interface, gvr schema.GroupVersionResource, objectName, objectNamespace, finalizer string, poll, timeout time.Duration) error { 696 framework.Logf("Waiting up to %v for object %s %s of resource %s to contain finalizer %s", timeout, objectNamespace, objectName, gvr.Resource, finalizer) 697 var ( 698 err error 699 resource *unstructured.Unstructured 700 ) 701 if successful := WaitUntil(poll, timeout, func() bool { 702 switch objectNamespace { 703 case "": 704 resource, err = c.Resource(gvr).Get(ctx, objectName, metav1.GetOptions{}) 705 default: 706 resource, err = c.Resource(gvr).Namespace(objectNamespace).Get(ctx, objectName, metav1.GetOptions{}) 707 } 708 if err != nil { 709 framework.Logf("Failed to get object %s %s with err: %v. Will retry in %v", objectNamespace, objectName, err, timeout) 710 return false 711 } 712 for _, f := range resource.GetFinalizers() { 713 if f == finalizer { 714 return true 715 } 716 } 717 return false 718 }); successful { 719 return nil 720 } 721 if err == nil { 722 err = fmt.Errorf("finalizer %s not added to object %s %s of resource %s", finalizer, objectNamespace, objectName, gvr) 723 } 724 return err 725 } 726 727 // VerifyFilePathGidInPod verfies expected GID of the target filepath 728 func VerifyFilePathGidInPod(f *framework.Framework, filePath, expectedGid string, pod *v1.Pod) { 729 cmd := fmt.Sprintf("ls -l %s", filePath) 730 stdout, stderr, err := e2evolume.PodExec(f, pod, cmd) 731 framework.ExpectNoError(err) 732 framework.Logf("pod %s/%s exec for cmd %s, stdout: %s, stderr: %s", pod.Namespace, pod.Name, cmd, stdout, stderr) 733 ll := strings.Fields(stdout) 734 framework.Logf("stdout split: %v, expected gid: %v", ll, expectedGid) 735 gomega.Expect(ll[3]).To(gomega.Equal(expectedGid)) 736 } 737 738 // ChangeFilePathGidInPod changes the GID of the target filepath. 739 func ChangeFilePathGidInPod(f *framework.Framework, filePath, targetGid string, pod *v1.Pod) { 740 cmd := fmt.Sprintf("chgrp %s %s", targetGid, filePath) 741 _, _, err := e2evolume.PodExec(f, pod, cmd) 742 framework.ExpectNoError(err) 743 VerifyFilePathGidInPod(f, filePath, targetGid, pod) 744 } 745 746 // DeleteStorageClass deletes the passed in StorageClass and catches errors other than "Not Found" 747 func DeleteStorageClass(ctx context.Context, cs clientset.Interface, className string) error { 748 err := cs.StorageV1().StorageClasses().Delete(ctx, className, metav1.DeleteOptions{}) 749 if err != nil && !apierrors.IsNotFound(err) { 750 return err 751 } 752 return nil 753 } 754 755 // CreateVolumeSource creates a volume source object 756 func CreateVolumeSource(pvcName string, readOnly bool) *v1.VolumeSource { 757 return &v1.VolumeSource{ 758 PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ 759 ClaimName: pvcName, 760 ReadOnly: readOnly, 761 }, 762 } 763 } 764 765 // TryFunc try to execute the function and return err if there is any 766 func TryFunc(f func()) error { 767 var err error 768 if f == nil { 769 return nil 770 } 771 defer func() { 772 if recoverError := recover(); recoverError != nil { 773 err = fmt.Errorf("%v", recoverError) 774 } 775 }() 776 f() 777 return err 778 } 779 780 // GetSizeRangesIntersection takes two instances of storage size ranges and determines the 781 // intersection of the intervals (if it exists) and return the minimum of the intersection 782 // to be used as the claim size for the test. 783 // if value not set, that means there's no minimum or maximum size limitation and we set default size for it. 784 func GetSizeRangesIntersection(first e2evolume.SizeRange, second e2evolume.SizeRange) (string, error) { 785 var firstMin, firstMax, secondMin, secondMax resource.Quantity 786 var err error 787 788 //if SizeRange is not set, assign a minimum or maximum size 789 if len(first.Min) == 0 { 790 first.Min = minValidSize 791 } 792 if len(first.Max) == 0 { 793 first.Max = maxValidSize 794 } 795 if len(second.Min) == 0 { 796 second.Min = minValidSize 797 } 798 if len(second.Max) == 0 { 799 second.Max = maxValidSize 800 } 801 802 if firstMin, err = resource.ParseQuantity(first.Min); err != nil { 803 return "", err 804 } 805 if firstMax, err = resource.ParseQuantity(first.Max); err != nil { 806 return "", err 807 } 808 if secondMin, err = resource.ParseQuantity(second.Min); err != nil { 809 return "", err 810 } 811 if secondMax, err = resource.ParseQuantity(second.Max); err != nil { 812 return "", err 813 } 814 815 interSectionStart := math.Max(float64(firstMin.Value()), float64(secondMin.Value())) 816 intersectionEnd := math.Min(float64(firstMax.Value()), float64(secondMax.Value())) 817 818 // the minimum of the intersection shall be returned as the claim size 819 var intersectionMin resource.Quantity 820 821 if intersectionEnd-interSectionStart >= 0 { //have intersection 822 intersectionMin = *resource.NewQuantity(int64(interSectionStart), "BinarySI") //convert value to BinarySI format. E.g. 5Gi 823 // return the minimum of the intersection as the claim size 824 return intersectionMin.String(), nil 825 } 826 return "", fmt.Errorf("intersection of size ranges %+v, %+v is null", first, second) 827 }