Add e2e test to check for filesystem volume device mount cleanup
This commit is contained in:
		| @@ -48,7 +48,7 @@ type Interface interface { | |||||||
| 	// most notably linux bind mounts and symbolic link. | 	// most notably linux bind mounts and symbolic link. | ||||||
| 	IsLikelyNotMountPoint(file string) (bool, error) | 	IsLikelyNotMountPoint(file string) (bool, error) | ||||||
| 	// GetMountRefs finds all mount references to the path, returns a | 	// GetMountRefs finds all mount references to the path, returns a | ||||||
| 	// list of paths. Path could be a mountpoint or a normal | 	// list of paths. Path could be a mountpoint path, device or a normal | ||||||
| 	// directory (for bind mount). | 	// directory (for bind mount). | ||||||
| 	GetMountRefs(pathname string) ([]string, error) | 	GetMountRefs(pathname string) ([]string, error) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -238,7 +238,7 @@ func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // GetMountRefs finds all mount references to pathname, returns a | // GetMountRefs finds all mount references to pathname, returns a | ||||||
| // list of paths. Path could be a mountpoint or a normal | // list of paths. Path could be a mountpoint path, device or a normal | ||||||
| // directory (for bind mount). | // directory (for bind mount). | ||||||
| func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) { | func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) { | ||||||
| 	pathExists, pathErr := PathExists(pathname) | 	pathExists, pathErr := PathExists(pathname) | ||||||
| @@ -441,8 +441,7 @@ func parseProcMounts(content []byte) ([]MountPoint, error) { | |||||||
|  |  | ||||||
| // SearchMountPoints finds all mount references to the source, returns a list of | // SearchMountPoints finds all mount references to the source, returns a list of | ||||||
| // mountpoints. | // mountpoints. | ||||||
| // The source can be a mount point or a normal directory (bind mount). We | // This function assumes source cannot be device. | ||||||
| // didn't support device because there is no use case by now. |  | ||||||
| // Some filesystems may share a source name, e.g. tmpfs. And for bind mounting, | // Some filesystems may share a source name, e.g. tmpfs. And for bind mounting, | ||||||
| // it's possible to mount a non-root path of a filesystem, so we need to use | // it's possible to mount a non-root path of a filesystem, so we need to use | ||||||
| // root path and major:minor to represent mount source uniquely. | // root path and major:minor to represent mount source uniquely. | ||||||
|   | |||||||
| @@ -157,24 +157,24 @@ func (s *subPathTestSuite) defineTests(driver TestDriver, pattern testpatterns.T | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	cleanup := func() { | 	cleanup := func() { | ||||||
| 		if l.pod != nil { | 		// if l.pod != nil { | ||||||
| 			ginkgo.By("Deleting pod") | 		// ginkgo.By("Deleting pod") | ||||||
| 			err := e2epod.DeletePodWithWait(f.ClientSet, l.pod) | 		// err := e2epod.DeletePodWithWait(f.ClientSet, l.pod) | ||||||
| 			framework.ExpectNoError(err, "while deleting pod") | 		// framework.ExpectNoError(err, "while deleting pod") | ||||||
| 			l.pod = nil | 		// l.pod = nil | ||||||
| 		} | 		// } | ||||||
|  |  | ||||||
| 		if l.resource != nil { | 		// if l.resource != nil { | ||||||
| 			l.resource.cleanupResource() | 		// l.resource.cleanupResource() | ||||||
| 			l.resource = nil | 		// l.resource = nil | ||||||
| 		} | 		// } | ||||||
|  |  | ||||||
| 		if l.driverCleanup != nil { | 		// if l.driverCleanup != nil { | ||||||
| 			l.driverCleanup() | 		// l.driverCleanup() | ||||||
| 			l.driverCleanup = nil | 		// l.driverCleanup = nil | ||||||
| 		} | 		// } | ||||||
|  |  | ||||||
| 		validateMigrationVolumeOpCounts(f.ClientSet, driver.GetDriverInfo().InTreePluginName, l.intreeOps, l.migratedOps) | 		// validateMigrationVolumeOpCounts(f.ClientSet, driver.GetDriverInfo().InTreePluginName, l.intreeOps, l.migratedOps) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ginkgo.It("should support non-existent path", func() { | 	ginkgo.It("should support non-existent path", func() { | ||||||
| @@ -902,7 +902,7 @@ func testSubpathReconstruction(f *framework.Framework, pod *v1.Pod, forceDelete | |||||||
| 	pod, err = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Get(pod.Name, metav1.GetOptions{}) | 	pod, err = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Get(pod.Name, metav1.GetOptions{}) | ||||||
| 	framework.ExpectNoError(err, "while getting pod") | 	framework.ExpectNoError(err, "while getting pod") | ||||||
|  |  | ||||||
| 	utils.TestVolumeUnmountsFromDeletedPodWithForceOption(f.ClientSet, f, pod, forceDelete, true) | 	utils.TestVolumeUnmountsFromDeletedPodWithForceOption(f.ClientSet, f, pod, forceDelete, true, true) | ||||||
| } | } | ||||||
|  |  | ||||||
| func formatVolume(f *framework.Framework, pod *v1.Pod) { | func formatVolume(f *framework.Framework, pod *v1.Pod) { | ||||||
|   | |||||||
| @@ -1,9 +1,6 @@ | |||||||
| package(default_visibility = ["//visibility:public"]) | package(default_visibility = ["//visibility:public"]) | ||||||
|  |  | ||||||
| load( | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") | ||||||
|     "@io_bazel_rules_go//go:def.bzl", |  | ||||||
|     "go_library", |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| go_library( | go_library( | ||||||
|     name = "go_default_library", |     name = "go_default_library", | ||||||
| @@ -16,6 +13,7 @@ go_library( | |||||||
|     ], |     ], | ||||||
|     importpath = "k8s.io/kubernetes/test/e2e/storage/utils", |     importpath = "k8s.io/kubernetes/test/e2e/storage/utils", | ||||||
|     deps = [ |     deps = [ | ||||||
|  |         "//pkg/util/mount:go_default_library", | ||||||
|         "//staging/src/k8s.io/api/apps/v1:go_default_library", |         "//staging/src/k8s.io/api/apps/v1:go_default_library", | ||||||
|         "//staging/src/k8s.io/api/core/v1:go_default_library", |         "//staging/src/k8s.io/api/core/v1:go_default_library", | ||||||
|         "//staging/src/k8s.io/api/rbac/v1:go_default_library", |         "//staging/src/k8s.io/api/rbac/v1:go_default_library", | ||||||
| @@ -23,6 +21,7 @@ go_library( | |||||||
|         "//staging/src/k8s.io/api/storage/v1beta1:go_default_library", |         "//staging/src/k8s.io/api/storage/v1beta1:go_default_library", | ||||||
|         "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", |         "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", | ||||||
|         "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", |         "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", | ||||||
|  |         "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", | ||||||
|         "//staging/src/k8s.io/apimachinery/pkg/util/uuid:go_default_library", |         "//staging/src/k8s.io/apimachinery/pkg/util/uuid:go_default_library", | ||||||
|         "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", |         "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", | ||||||
|         "//staging/src/k8s.io/client-go/kubernetes:go_default_library", |         "//staging/src/k8s.io/client-go/kubernetes:go_default_library", | ||||||
| @@ -49,3 +48,10 @@ filegroup( | |||||||
|     srcs = [":package-srcs"], |     srcs = [":package-srcs"], | ||||||
|     tags = ["automanaged"], |     tags = ["automanaged"], | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | go_test( | ||||||
|  |     name = "go_default_test", | ||||||
|  |     srcs = ["utils_test.go"], | ||||||
|  |     embed = [":go_default_library"], | ||||||
|  |     deps = ["//vendor/github.com/onsi/gomega:go_default_library"], | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -20,7 +20,9 @@ import ( | |||||||
| 	"crypto/sha256" | 	"crypto/sha256" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"math/rand" | 	"math/rand" | ||||||
|  | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -34,6 +36,7 @@ import ( | |||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/util/wait" | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
| 	clientset "k8s.io/client-go/kubernetes" | 	clientset "k8s.io/client-go/kubernetes" | ||||||
|  | 	"k8s.io/kubernetes/pkg/util/mount" | ||||||
| 	"k8s.io/kubernetes/test/e2e/framework" | 	"k8s.io/kubernetes/test/e2e/framework" | ||||||
| 	e2enode "k8s.io/kubernetes/test/e2e/framework/node" | 	e2enode "k8s.io/kubernetes/test/e2e/framework/node" | ||||||
| 	e2epod "k8s.io/kubernetes/test/e2e/framework/pod" | 	e2epod "k8s.io/kubernetes/test/e2e/framework/pod" | ||||||
| @@ -234,9 +237,48 @@ func TestKubeletRestartsAndRestoresMap(c clientset.Interface, f *framework.Frame | |||||||
| 	framework.Logf("Volume map detected on pod %s and written data %s is readable post-restart.", clientPod.Name, path) | 	framework.Logf("Volume map detected on pod %s and written data %s is readable post-restart.", clientPod.Name, path) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // findGlobalVolumeMountPaths finds all global volume mount paths for given pod from the host mount information. | ||||||
|  | // This function assumes: | ||||||
|  | // 1) pod volume mount paths exists in /var/lib/kubelet/pods/<pod-uid>/volumes/ | ||||||
|  | // 2) global volume mount paths exists in /var/lib/kubelet/plugins/ | ||||||
|  | func findGlobalVolumeMountPaths(mountInfo string, podUID string) ([]string, error) { | ||||||
|  | 	tmpfile, err := ioutil.TempFile("", "mountinfo") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	defer os.Remove(tmpfile.Name()) // clean up | ||||||
|  | 	err = ioutil.WriteFile(tmpfile.Name(), []byte(mountInfo), 0644) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	podVolumeMountBase := fmt.Sprintf("/var/lib/kubelet/pods/%s/volumes/", podUID) | ||||||
|  | 	globalVolumeMountBase := "/var/lib/kubelet/plugins" | ||||||
|  | 	mis, err := mount.ParseMountInfo(tmpfile.Name()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	globalVolumeMountPaths := []string{} | ||||||
|  | 	for _, mi := range mis { | ||||||
|  | 		if mount.PathWithinBase(mi.MountPoint, podVolumeMountBase) { | ||||||
|  | 			refs, err := mount.SearchMountPoints(mi.MountPoint, tmpfile.Name()) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			for _, ref := range refs { | ||||||
|  | 				if mount.PathWithinBase(ref, globalVolumeMountBase) { | ||||||
|  | 					globalVolumeMountPaths = append(globalVolumeMountPaths, ref) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return globalVolumeMountPaths, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // TestVolumeUnmountsFromDeletedPodWithForceOption tests that a volume unmounts if the client pod was deleted while the kubelet was down. | // TestVolumeUnmountsFromDeletedPodWithForceOption tests that a volume unmounts if the client pod was deleted while the kubelet was down. | ||||||
| // forceDelete is true indicating whether the pod is forcefully deleted. | // forceDelete is true indicating whether the pod is forcefully deleted. | ||||||
| func TestVolumeUnmountsFromDeletedPodWithForceOption(c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, forceDelete bool, checkSubpath bool) { | // checkSubpath is true indicating whether the subpath should be checked. | ||||||
|  | // checkGlobalMount is true indicating whether the global mount should be checked. | ||||||
|  | func TestVolumeUnmountsFromDeletedPodWithForceOption(c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, forceDelete bool, checkSubpath bool, checkGlobalMount bool) { | ||||||
| 	nodeIP, err := framework.GetHostAddress(c, clientPod) | 	nodeIP, err := framework.GetHostAddress(c, clientPod) | ||||||
| 	framework.ExpectNoError(err) | 	framework.ExpectNoError(err) | ||||||
| 	nodeIP = nodeIP + ":22" | 	nodeIP = nodeIP + ":22" | ||||||
| @@ -255,6 +297,24 @@ func TestVolumeUnmountsFromDeletedPodWithForceOption(c clientset.Interface, f *f | |||||||
| 		gomega.Expect(result.Code).To(gomega.BeZero(), fmt.Sprintf("Expected grep exit code of 0, got %d", result.Code)) | 		gomega.Expect(result.Code).To(gomega.BeZero(), fmt.Sprintf("Expected grep exit code of 0, got %d", result.Code)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	var globalVolumeMountPaths []string | ||||||
|  | 	if checkGlobalMount { | ||||||
|  | 		// Find global mount path and verify it will be unmounted later. | ||||||
|  | 		// We don't verify it must exist because: | ||||||
|  | 		// 1) not all volume types have global mount path, e.g. local filesystem volume with directory source | ||||||
|  | 		// 2) volume types which failed to mount global mount path will fail in other test | ||||||
|  | 		ginkgo.By("Find the volume global mount paths") | ||||||
|  | 		result, err = e2essh.SSH("cat /proc/self/mountinfo", nodeIP, framework.TestContext.Provider) | ||||||
|  | 		framework.ExpectNoError(err, "Encountered SSH error.") | ||||||
|  | 		globalVolumeMountPaths, err = findGlobalVolumeMountPaths(result.Stdout, string(clientPod.UID)) | ||||||
|  | 		framework.ExpectNoError(err, fmt.Sprintf("Failed to get global volume mount paths: %v", err)) | ||||||
|  | 		if len(globalVolumeMountPaths) > 0 { | ||||||
|  | 			framework.Logf("Volume global mount paths found at %v", globalVolumeMountPaths) | ||||||
|  | 		} else { | ||||||
|  | 			framework.Logf("No volume global mount paths found") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// This command is to make sure kubelet is started after test finishes no matter it fails or not. | 	// This command is to make sure kubelet is started after test finishes no matter it fails or not. | ||||||
| 	defer func() { | 	defer func() { | ||||||
| 		KubeletCommand(KStart, c, clientPod) | 		KubeletCommand(KStart, c, clientPod) | ||||||
| @@ -298,16 +358,28 @@ func TestVolumeUnmountsFromDeletedPodWithForceOption(c clientset.Interface, f *f | |||||||
| 		gomega.Expect(result.Stdout).To(gomega.BeEmpty(), "Expected grep stdout to be empty (i.e. no subpath mount found).") | 		gomega.Expect(result.Stdout).To(gomega.BeEmpty(), "Expected grep stdout to be empty (i.e. no subpath mount found).") | ||||||
| 		framework.Logf("Subpath volume unmounted on node %s", clientPod.Spec.NodeName) | 		framework.Logf("Subpath volume unmounted on node %s", clientPod.Spec.NodeName) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if checkGlobalMount && len(globalVolumeMountPaths) > 0 { | ||||||
|  | 		globalMountPathCmd := fmt.Sprintf("ls %s | grep '.'", strings.Join(globalVolumeMountPaths, " ")) | ||||||
|  | 		if isSudoPresent(nodeIP, framework.TestContext.Provider) { | ||||||
|  | 			globalMountPathCmd = fmt.Sprintf("sudo sh -c \"%s\"", globalMountPathCmd) | ||||||
|  | 		} | ||||||
|  | 		ginkgo.By("Expecting the volume global mount path not to be found.") | ||||||
|  | 		result, err = e2essh.SSH(globalMountPathCmd, nodeIP, framework.TestContext.Provider) | ||||||
|  | 		e2essh.LogResult(result) | ||||||
|  | 		framework.ExpectNoError(err, "Encountered SSH error.") | ||||||
|  | 		gomega.Expect(result.Stdout).To(gomega.BeEmpty(), "Expected grep stdout to be empty.") | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // TestVolumeUnmountsFromDeletedPod tests that a volume unmounts if the client pod was deleted while the kubelet was down. | // TestVolumeUnmountsFromDeletedPod tests that a volume unmounts if the client pod was deleted while the kubelet was down. | ||||||
| func TestVolumeUnmountsFromDeletedPod(c clientset.Interface, f *framework.Framework, clientPod *v1.Pod) { | func TestVolumeUnmountsFromDeletedPod(c clientset.Interface, f *framework.Framework, clientPod *v1.Pod) { | ||||||
| 	TestVolumeUnmountsFromDeletedPodWithForceOption(c, f, clientPod, false, false) | 	TestVolumeUnmountsFromDeletedPodWithForceOption(c, f, clientPod, false, false, false) | ||||||
| } | } | ||||||
|  |  | ||||||
| // TestVolumeUnmountsFromForceDeletedPod tests that a volume unmounts if the client pod was forcefully deleted while the kubelet was down. | // TestVolumeUnmountsFromForceDeletedPod tests that a volume unmounts if the client pod was forcefully deleted while the kubelet was down. | ||||||
| func TestVolumeUnmountsFromForceDeletedPod(c clientset.Interface, f *framework.Framework, clientPod *v1.Pod) { | func TestVolumeUnmountsFromForceDeletedPod(c clientset.Interface, f *framework.Framework, clientPod *v1.Pod) { | ||||||
| 	TestVolumeUnmountsFromDeletedPodWithForceOption(c, f, clientPod, true, false) | 	TestVolumeUnmountsFromDeletedPodWithForceOption(c, f, clientPod, true, false, false) | ||||||
| } | } | ||||||
|  |  | ||||||
| // TestVolumeUnmapsFromDeletedPodWithForceOption tests that a volume unmaps if the client pod was deleted while the kubelet was down. | // TestVolumeUnmapsFromDeletedPodWithForceOption tests that a volume unmaps if the client pod was deleted while the kubelet was down. | ||||||
|   | |||||||
							
								
								
									
										56
									
								
								test/e2e/storage/utils/utils_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								test/e2e/storage/utils/utils_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2019 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package utils | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/onsi/gomega" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestFindGlobalVolumeMountPaths(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		mountInfo string | ||||||
|  | 		podUID    string | ||||||
|  | 		expected  []string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "pod uses local filesystem pv with block source", | ||||||
|  | 			mountInfo: `1045 245 0:385 / /var/lib/kubelet/pods/ff5e9fa2-7111-486d-854c-848bcc6b3819/volumes/kubernetes.io~secret/default-token-djlt2 rw,relatime shared:199 - tmpfs tmpfs rw | ||||||
|  | 1047 245 7:6 / /var/lib/kubelet/plugins/kubernetes.io/local-volume/mounts/local-wdx8b rw,relatime shared:200 - ext4 /dev/loop6 rw,data=ordered | ||||||
|  | 1048 245 7:6 / /var/lib/kubelet/pods/ff5e9fa2-7111-486d-854c-848bcc6b3819/volumes/kubernetes.io~local-volume/local-wdx8b rw,relatime shared:200 - ext4 /dev/loop6 rw,data=ordered | ||||||
|  | 1054 245 7:6 /provisioning-9823 /var/lib/kubelet/pods/ff5e9fa2-7111-486d-854c-848bcc6b3819/volume-subpaths/local-wdx8b/test-container-subpath-local-preprovisionedpv-d72p/0 rw,relatime shared:200 - ext4 /dev/loop6 rw,data=ordered | ||||||
|  | `, | ||||||
|  | 			podUID: "ff5e9fa2-7111-486d-854c-848bcc6b3819", | ||||||
|  | 			expected: []string{ | ||||||
|  | 				"/var/lib/kubelet/plugins/kubernetes.io/local-volume/mounts/local-wdx8b", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	g := gomega.NewWithT(t) | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			mountPaths, err := findGlobalVolumeMountPaths(tt.mountInfo, tt.podUID) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			g.Expect(mountPaths).To(gomega.ConsistOf(tt.expected)) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Yecheng Fu
					Yecheng Fu