diff --git a/hack/verify-flags/exceptions.txt b/hack/verify-flags/exceptions.txt index a6d5a48813b..78a813bca8c 100644 --- a/hack/verify-flags/exceptions.txt +++ b/hack/verify-flags/exceptions.txt @@ -92,3 +92,4 @@ test/e2e/host_path.go: fmt.Sprintf("--retry_time=%d", retryDuration), test/images/mount-tester/mt.go: flag.BoolVar(&breakOnExpectedContent, "break_on_expected_content", true, "Break out of loop on expected content, (use with --file_content_in_loop flag only)") test/images/mount-tester/mt.go: flag.IntVar(&retryDuration, "retry_time", 180, "Retry time during the loop") test/images/mount-tester/mt.go: flag.StringVar(&readFileContentInLoopPath, "file_content_in_loop", "", "Path to read the file content in loop from") +test/e2e/host_path.go: fmt.Sprintf("--file_content_in_loop=%v", filePathInReader), diff --git a/pkg/api/types.go b/pkg/api/types.go index 31fd337b8cd..03acad36546 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -758,6 +758,9 @@ type VolumeMount struct { ReadOnly bool `json:"readOnly,omitempty"` // Required. Must not contain ':'. MountPath string `json:"mountPath"` + // Path within the volume from which the container's volume should be mounted. + // Defaults to "" (volume's root). + SubPath string `json:"subPath,omitempty"` } // EnvVar represents an environment variable present in a Container. diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 93416b587f0..b11ffed3d97 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -883,6 +883,9 @@ type VolumeMount struct { // Path within the container at which the volume should be mounted. Must // not contain ':'. MountPath string `json:"mountPath" protobuf:"bytes,3,opt,name=mountPath"` + // Path within the volume from which the container's volume should be mounted. + // Defaults to "" (volume's root). + SubPath string `json:"subPath,omitempty" protobuf:"bytes,4,opt,name=subPath"` } // EnvVar represents an environment variable present in a Container. diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 95d729cb2a5..0c15f53b3d6 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -746,6 +746,28 @@ func validateDownwardAPIVolumeSource(downwardAPIVolume *api.DownwardAPIVolumeSou return allErrs } +// This validate will make sure targetPath: +// 1. is not abs path +// 2. does not start with '../' +// 3. does not contain '/../' +// 4. does not end with '/..' +func validateSubPath(targetPath string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if path.IsAbs(targetPath) { + allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must be a relative path")) + } + if strings.HasPrefix(targetPath, "../") { + allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must not start with '../'")) + } + if strings.Contains(targetPath, "/../") { + allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must not contain '/../'")) + } + if strings.HasSuffix(targetPath, "/..") { + allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must not end with '/..'")) + } + return allErrs +} + // This validate will make sure targetPath: // 1. is not abs path // 2. does not contain '..' @@ -1168,6 +1190,9 @@ func validateVolumeMounts(mounts []api.VolumeMount, volumes sets.String, fldPath allErrs = append(allErrs, field.Invalid(idxPath.Child("mountPath"), mnt.MountPath, "must be unique")) } mountpoints.Insert(mnt.MountPath) + if len(mnt.SubPath) > 0 { + allErrs = append(allErrs, validateSubPath(mnt.SubPath, fldPath.Child("subPath"))...) + } } return allErrs } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 60d35ec0b42..5859a9c7784 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -1274,6 +1274,10 @@ func TestValidateVolumeMounts(t *testing.T) { {Name: "abc", MountPath: "/foo"}, {Name: "123", MountPath: "/bar"}, {Name: "abc-123", MountPath: "/baz"}, + {Name: "abc-123", MountPath: "/baa", SubPath: ""}, + {Name: "abc-123", MountPath: "/bab", SubPath: "baz"}, + {Name: "abc-123", MountPath: "/bac", SubPath: ".baz"}, + {Name: "abc-123", MountPath: "/bad", SubPath: "..baz"}, } if errs := validateVolumeMounts(successCase, volumes, field.NewPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) @@ -1285,6 +1289,10 @@ func TestValidateVolumeMounts(t *testing.T) { "empty mountpath": {{Name: "abc", MountPath: ""}}, "colon mountpath": {{Name: "abc", MountPath: "foo:bar"}}, "mountpath collision": {{Name: "foo", MountPath: "/path/a"}, {Name: "bar", MountPath: "/path/a"}}, + "absolute subpath": {{Name: "abc", MountPath: "/bar", SubPath: "/baz"}}, + "subpath in ..": {{Name: "abc", MountPath: "/bar", SubPath: "../baz"}}, + "subpath contains ..": {{Name: "abc", MountPath: "/bar", SubPath: "baz/../bat"}}, + "subpath ends in ..": {{Name: "abc", MountPath: "/bar", SubPath: "./.."}}, } for k, v := range errorCases { if errs := validateVolumeMounts(v, volumes, field.NewPath("field")); len(errs) == 0 { diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index c6be6894ad1..4d301b96902 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -1176,10 +1176,14 @@ func makeMounts(pod *api.Pod, podDir string, container *api.Container, hostName, vol.SELinuxLabeled = true relabelVolume = true } + hostPath := vol.Mounter.GetPath() + if mount.SubPath != "" { + hostPath = filepath.Join(hostPath, mount.SubPath) + } mounts = append(mounts, kubecontainer.Mount{ Name: mount.Name, ContainerPath: mount.MountPath, - HostPath: vol.Mounter.GetPath(), + HostPath: hostPath, ReadOnly: mount.ReadOnly, SELinuxRelabel: relabelVolume, }) diff --git a/test/e2e/host_path.go b/test/e2e/host_path.go index 58a5a1dc848..3f264b6b88c 100644 --- a/test/e2e/host_path.go +++ b/test/e2e/host_path.go @@ -88,6 +88,37 @@ var _ = framework.KubeDescribe("hostPath", func() { }, namespace.Name, ) }) + + It("should support subPath [Conformance]", func() { + volumePath := "/test-volume" + subPath := "sub-path" + fileName := "test-file" + retryDuration := 180 + + filePathInWriter := path.Join(volumePath, fileName) + filePathInReader := path.Join(volumePath, subPath, fileName) + + source := &api.HostPathVolumeSource{ + Path: "/tmp", + } + pod := testPodWithHostVol(volumePath, source) + // Write the file in the subPath from container 0 + container := &pod.Spec.Containers[0] + container.VolumeMounts[0].SubPath = subPath + container.Args = []string{ + fmt.Sprintf("--new_file_0644=%v", filePathInWriter), + fmt.Sprintf("--file_mode=%v", filePathInWriter), + } + // Read it from outside the subPath from container 1 + pod.Spec.Containers[1].Args = []string{ + fmt.Sprintf("--file_content_in_loop=%v", filePathInReader), + fmt.Sprintf("--retry_time=%d", retryDuration), + } + + framework.TestContainerOutput("hostPath subPath", c, pod, 1, []string{ + "content of file \"" + filePathInReader + "\": mount-tester new file", + }, namespace.Name) + }) }) //These constants are borrowed from the other test.