Commit a64b8541 authored by Kevin Taylor's avatar Kevin Taylor

Implementation of KEP Feature Gate VolumeSubpathEnvExpansion

parent f7c4389b
...@@ -11058,6 +11058,10 @@ ...@@ -11058,6 +11058,10 @@
"subPath": { "subPath": {
"description": "Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).", "description": "Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).",
"type": "string" "type": "string"
},
"subPathExpr": {
"description": "Expanded path within the volume from which the container's volume should be mounted. Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. Defaults to \"\" (volume's root). SubPathExpr and SubPath are mutually exclusive. This field is alpha in 1.14.",
"type": "string"
} }
}, },
"required": [ "required": [
...@@ -350,6 +350,20 @@ func dropDisabledFields( ...@@ -350,6 +350,20 @@ func dropDisabledFields(
} }
} }
if (!utilfeature.DefaultFeatureGate.Enabled(features.VolumeSubpath) || !utilfeature.DefaultFeatureGate.Enabled(features.VolumeSubpathEnvExpansion)) && !subpathExprInUse(oldPodSpec) {
// drop subpath env expansion from the pod if either of the subpath features is disabled and the old spec did not specify subpath env expansion
for i := range podSpec.Containers {
for j := range podSpec.Containers[i].VolumeMounts {
podSpec.Containers[i].VolumeMounts[j].SubPathExpr = ""
}
}
for i := range podSpec.InitContainers {
for j := range podSpec.InitContainers[i].VolumeMounts {
podSpec.InitContainers[i].VolumeMounts[j].SubPathExpr = ""
}
}
}
dropDisabledVolumeDevicesFields(podSpec, oldPodSpec) dropDisabledVolumeDevicesFields(podSpec, oldPodSpec)
dropDisabledRunAsGroupField(podSpec, oldPodSpec) dropDisabledRunAsGroupField(podSpec, oldPodSpec)
...@@ -595,3 +609,25 @@ func runAsGroupInUse(podSpec *api.PodSpec) bool { ...@@ -595,3 +609,25 @@ func runAsGroupInUse(podSpec *api.PodSpec) bool {
} }
return false return false
} }
// subpathExprInUse returns true if the pod spec is non-nil and has a volume mount that makes use of the subPathExpr feature
func subpathExprInUse(podSpec *api.PodSpec) bool {
if podSpec == nil {
return false
}
for i := range podSpec.Containers {
for j := range podSpec.Containers[i].VolumeMounts {
if len(podSpec.Containers[i].VolumeMounts[j].SubPathExpr) > 0 {
return true
}
}
}
for i := range podSpec.InitContainers {
for j := range podSpec.InitContainers[i].VolumeMounts {
if len(podSpec.InitContainers[i].VolumeMounts[j].SubPathExpr) > 0 {
return true
}
}
}
return false
}
...@@ -1549,3 +1549,97 @@ func TestDropPodSysctls(t *testing.T) { ...@@ -1549,3 +1549,97 @@ func TestDropPodSysctls(t *testing.T) {
} }
} }
} }
func TestDropSubPathExpr(t *testing.T) {
podWithSubpaths := func() *api.Pod {
return &api.Pod{
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyNever,
Containers: []api.Container{{Name: "container1", Image: "testimage", VolumeMounts: []api.VolumeMount{{Name: "a", SubPathExpr: "foo"}, {Name: "a", SubPathExpr: "foo2"}, {Name: "a", SubPathExpr: "foo3"}}}},
InitContainers: []api.Container{{Name: "container1", Image: "testimage", VolumeMounts: []api.VolumeMount{{Name: "a", SubPathExpr: "foo"}, {Name: "a", SubPathExpr: "foo2"}}}},
Volumes: []api.Volume{{Name: "a", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{Path: "/dev/xvdc"}}}},
},
}
}
podWithoutSubpaths := func() *api.Pod {
return &api.Pod{
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyNever,
Containers: []api.Container{{Name: "container1", Image: "testimage", VolumeMounts: []api.VolumeMount{{Name: "a", SubPathExpr: ""}, {Name: "a", SubPathExpr: ""}, {Name: "a", SubPathExpr: ""}}}},
InitContainers: []api.Container{{Name: "container1", Image: "testimage", VolumeMounts: []api.VolumeMount{{Name: "a", SubPathExpr: ""}, {Name: "a", SubPathExpr: ""}}}},
Volumes: []api.Volume{{Name: "a", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{Path: "/dev/xvdc"}}}},
},
}
}
podInfo := []struct {
description string
hasSubpaths bool
pod func() *api.Pod
}{
{
description: "has subpaths",
hasSubpaths: true,
pod: podWithSubpaths,
},
{
description: "does not have subpaths",
hasSubpaths: false,
pod: podWithoutSubpaths,
},
{
description: "is nil",
hasSubpaths: false,
pod: func() *api.Pod { return nil },
},
}
for _, enabled := range []bool{true, false} {
for _, oldPodInfo := range podInfo {
for _, newPodInfo := range podInfo {
oldPodHasSubpaths, oldPod := oldPodInfo.hasSubpaths, oldPodInfo.pod()
newPodHasSubpaths, newPod := newPodInfo.hasSubpaths, newPodInfo.pod()
if newPod == nil {
continue
}
t.Run(fmt.Sprintf("feature enabled=%v, old pod %v, new pod %v", enabled, oldPodInfo.description, newPodInfo.description), func(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeSubpathEnvExpansion, enabled)()
var oldPodSpec *api.PodSpec
if oldPod != nil {
oldPodSpec = &oldPod.Spec
}
dropDisabledFields(&newPod.Spec, nil, oldPodSpec, nil)
// old pod should never be changed
if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) {
t.Errorf("old pod changed: %v", diff.ObjectReflectDiff(oldPod, oldPodInfo.pod()))
}
switch {
case enabled || oldPodHasSubpaths:
// new pod should not be changed if the feature is enabled, or if the old pod had subpaths
if !reflect.DeepEqual(newPod, newPodInfo.pod()) {
t.Errorf("new pod changed: %v", diff.ObjectReflectDiff(newPod, newPodInfo.pod()))
}
case newPodHasSubpaths:
// new pod should be changed
if reflect.DeepEqual(newPod, newPodInfo.pod()) {
t.Errorf("new pod was not changed")
}
// new pod should not have subpaths
if !reflect.DeepEqual(newPod, podWithoutSubpaths()) {
t.Errorf("new pod had subpaths: %v", diff.ObjectReflectDiff(newPod, podWithoutSubpaths()))
}
default:
// new pod should not need to be changed
if !reflect.DeepEqual(newPod, newPodInfo.pod()) {
t.Errorf("new pod changed: %v", diff.ObjectReflectDiff(newPod, newPodInfo.pod()))
}
}
})
}
}
}
}
...@@ -1644,6 +1644,13 @@ type VolumeMount struct { ...@@ -1644,6 +1644,13 @@ type VolumeMount struct {
// This field is beta in 1.10. // This field is beta in 1.10.
// +optional // +optional
MountPropagation *MountPropagationMode MountPropagation *MountPropagationMode
// Expanded path within the volume from which the container's volume should be mounted.
// Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment.
// Defaults to "" (volume's root).
// SubPathExpr and SubPath are mutually exclusive.
// This field is alpha in 1.14.
// +optional
SubPathExpr string
} }
// MountPropagationMode describes mount propagation. // MountPropagationMode describes mount propagation.
......
...@@ -7372,6 +7372,7 @@ func autoConvert_v1_VolumeMount_To_core_VolumeMount(in *v1.VolumeMount, out *cor ...@@ -7372,6 +7372,7 @@ func autoConvert_v1_VolumeMount_To_core_VolumeMount(in *v1.VolumeMount, out *cor
out.MountPath = in.MountPath out.MountPath = in.MountPath
out.SubPath = in.SubPath out.SubPath = in.SubPath
out.MountPropagation = (*core.MountPropagationMode)(unsafe.Pointer(in.MountPropagation)) out.MountPropagation = (*core.MountPropagationMode)(unsafe.Pointer(in.MountPropagation))
out.SubPathExpr = in.SubPathExpr
return nil return nil
} }
...@@ -7386,6 +7387,7 @@ func autoConvert_core_VolumeMount_To_v1_VolumeMount(in *core.VolumeMount, out *v ...@@ -7386,6 +7387,7 @@ func autoConvert_core_VolumeMount_To_v1_VolumeMount(in *core.VolumeMount, out *v
out.MountPath = in.MountPath out.MountPath = in.MountPath
out.SubPath = in.SubPath out.SubPath = in.SubPath
out.MountPropagation = (*v1.MountPropagationMode)(unsafe.Pointer(in.MountPropagation)) out.MountPropagation = (*v1.MountPropagationMode)(unsafe.Pointer(in.MountPropagation))
out.SubPathExpr = in.SubPathExpr
return nil return nil
} }
......
...@@ -2264,6 +2264,14 @@ func ValidateVolumeMounts(mounts []core.VolumeMount, voldevices map[string]strin ...@@ -2264,6 +2264,14 @@ func ValidateVolumeMounts(mounts []core.VolumeMount, voldevices map[string]strin
allErrs = append(allErrs, validateLocalDescendingPath(mnt.SubPath, fldPath.Child("subPath"))...) allErrs = append(allErrs, validateLocalDescendingPath(mnt.SubPath, fldPath.Child("subPath"))...)
} }
if len(mnt.SubPathExpr) > 0 {
if len(mnt.SubPath) > 0 {
allErrs = append(allErrs, field.Invalid(idxPath.Child("subPathExpr"), mnt.SubPathExpr, "subPathExpr and subPath are mutually exclusive"))
}
allErrs = append(allErrs, validateLocalDescendingPath(mnt.SubPathExpr, fldPath.Child("subPathExpr"))...)
}
if mnt.MountPropagation != nil { if mnt.MountPropagation != nil {
allErrs = append(allErrs, validateMountPropagation(mnt.MountPropagation, container, fldPath.Child("mountPropagation"))...) allErrs = append(allErrs, validateMountPropagation(mnt.MountPropagation, container, fldPath.Child("mountPropagation"))...)
} }
......
...@@ -4816,6 +4816,230 @@ func TestValidateDisabledSubpath(t *testing.T) { ...@@ -4816,6 +4816,230 @@ func TestValidateDisabledSubpath(t *testing.T) {
} }
} }
func TestValidateSubpathMutuallyExclusive(t *testing.T) {
// Enable feature VolumeSubpathEnvExpansion and VolumeSubpath
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeSubpathEnvExpansion, true)()
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeSubpath, true)()
volumes := []core.Volume{
{Name: "abc", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim1"}}},
{Name: "abc-123", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim2"}}},
{Name: "123", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}},
}
vols, v1err := ValidateVolumes(volumes, field.NewPath("field"))
if len(v1err) > 0 {
t.Errorf("Invalid test volume - expected success %v", v1err)
return
}
container := core.Container{
SecurityContext: nil,
}
goodVolumeDevices := []core.VolumeDevice{
{Name: "xyz", DevicePath: "/foofoo"},
{Name: "uvw", DevicePath: "/foofoo/share/test"},
}
cases := map[string]struct {
mounts []core.VolumeMount
expectError bool
}{
"subpath and subpathexpr not specified": {
[]core.VolumeMount{
{
Name: "abc-123",
MountPath: "/bab",
},
},
false,
},
"subpath expr specified": {
[]core.VolumeMount{
{
Name: "abc-123",
MountPath: "/bab",
SubPathExpr: "$(POD_NAME)",
},
},
false,
},
"subpath specified": {
[]core.VolumeMount{
{
Name: "abc-123",
MountPath: "/bab",
SubPath: "baz",
},
},
false,
},
"subpath and subpathexpr specified": {
[]core.VolumeMount{
{
Name: "abc-123",
MountPath: "/bab",
SubPath: "baz",
SubPathExpr: "$(POD_NAME)",
},
},
true,
},
}
for name, test := range cases {
errs := ValidateVolumeMounts(test.mounts, GetVolumeDeviceMap(goodVolumeDevices), vols, &container, field.NewPath("field"))
if len(errs) != 0 && !test.expectError {
t.Errorf("test %v failed: %+v", name, errs)
}
if len(errs) == 0 && test.expectError {
t.Errorf("test %v failed, expected error", name)
}
}
}
func TestValidateDisabledSubpathExpr(t *testing.T) {
// Enable feature VolumeSubpathEnvExpansion
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeSubpathEnvExpansion, true)()
volumes := []core.Volume{
{Name: "abc", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim1"}}},
{Name: "abc-123", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim2"}}},
{Name: "123", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}},
}
vols, v1err := ValidateVolumes(volumes, field.NewPath("field"))
if len(v1err) > 0 {
t.Errorf("Invalid test volume - expected success %v", v1err)
return
}
container := core.Container{
SecurityContext: nil,
}
goodVolumeDevices := []core.VolumeDevice{
{Name: "xyz", DevicePath: "/foofoo"},
{Name: "uvw", DevicePath: "/foofoo/share/test"},
}
cases := map[string]struct {
mounts []core.VolumeMount
expectError bool
}{
"subpath expr not specified": {
[]core.VolumeMount{
{
Name: "abc-123",
MountPath: "/bab",
},
},
false,
},
"subpath expr specified": {
[]core.VolumeMount{
{
Name: "abc-123",
MountPath: "/bab",
SubPathExpr: "$(POD_NAME)",
},
},
false,
},
}
for name, test := range cases {
errs := ValidateVolumeMounts(test.mounts, GetVolumeDeviceMap(goodVolumeDevices), vols, &container, field.NewPath("field"))
if len(errs) != 0 && !test.expectError {
t.Errorf("test %v failed: %+v", name, errs)
}
if len(errs) == 0 && test.expectError {
t.Errorf("test %v failed, expected error", name)
}
}
// Repeat with feature gate off
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeSubpathEnvExpansion, false)()
cases = map[string]struct {
mounts []core.VolumeMount
expectError bool
}{
"subpath expr not specified": {
[]core.VolumeMount{
{
Name: "abc-123",
MountPath: "/bab",
},
},
false,
},
"subpath expr specified": {
[]core.VolumeMount{
{
Name: "abc-123",
MountPath: "/bab",
SubPathExpr: "$(POD_NAME)",
},
},
false, // validation should not fail, dropping the field is handled in PrepareForCreate/PrepareForUpdate
},
}
for name, test := range cases {
errs := ValidateVolumeMounts(test.mounts, GetVolumeDeviceMap(goodVolumeDevices), vols, &container, field.NewPath("field"))
if len(errs) != 0 && !test.expectError {
t.Errorf("test %v failed: %+v", name, errs)
}
if len(errs) == 0 && test.expectError {
t.Errorf("test %v failed, expected error", name)
}
}
// Repeat with subpath feature gate off
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeSubpath, false)()
cases = map[string]struct {
mounts []core.VolumeMount
expectError bool
}{
"subpath expr not specified": {
[]core.VolumeMount{
{
Name: "abc-123",
MountPath: "/bab",
},
},
false,
},
"subpath expr specified": {
[]core.VolumeMount{
{
Name: "abc-123",
MountPath: "/bab",
SubPathExpr: "$(POD_NAME)",
},
},
false, // validation should not fail, dropping the field is handled in PrepareForCreate/PrepareForUpdate
},
}
for name, test := range cases {
errs := ValidateVolumeMounts(test.mounts, GetVolumeDeviceMap(goodVolumeDevices), vols, &container, field.NewPath("field"))
if len(errs) != 0 && !test.expectError {
t.Errorf("test %v failed: %+v", name, errs)
}
if len(errs) == 0 && test.expectError {
t.Errorf("test %v failed, expected error", name)
}
}
}
func TestValidateMountPropagation(t *testing.T) { func TestValidateMountPropagation(t *testing.T) {
bTrue := true bTrue := true
bFalse := false bFalse := false
......
...@@ -29,6 +29,7 @@ go_library( ...@@ -29,6 +29,7 @@ go_library(
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/client-go/tools/record:go_default_library", "//staging/src/k8s.io/client-go/tools/record:go_default_library",
"//staging/src/k8s.io/client-go/tools/reference:go_default_library", "//staging/src/k8s.io/client-go/tools/reference:go_default_library",
"//staging/src/k8s.io/client-go/tools/remotecommand:go_default_library", "//staging/src/k8s.io/client-go/tools/remotecommand:go_default_library",
......
...@@ -27,6 +27,7 @@ import ( ...@@ -27,6 +27,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/tools/record" "k8s.io/client-go/tools/record"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2" runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2"
"k8s.io/kubernetes/pkg/kubelet/util/format" "k8s.io/kubernetes/pkg/kubelet/util/format"
...@@ -130,9 +131,22 @@ func ExpandContainerCommandOnlyStatic(containerCommand []string, envs []v1.EnvVa ...@@ -130,9 +131,22 @@ func ExpandContainerCommandOnlyStatic(containerCommand []string, envs []v1.EnvVa
return command return command
} }
func ExpandContainerVolumeMounts(mount v1.VolumeMount, envs []EnvVar) (expandedSubpath string) { func ExpandContainerVolumeMounts(mount v1.VolumeMount, envs []EnvVar) (string, error) {
mapping := expansion.MappingFuncFor(EnvVarsToMap(envs))
return expansion.Expand(mount.SubPath, mapping) envmap := EnvVarsToMap(envs)
missingKeys := sets.NewString()
expanded := expansion.Expand(mount.SubPathExpr, func(key string) string {
value, ok := envmap[key]
if !ok || len(value) == 0 {
missingKeys.Insert(key)
}
return value
})
if len(missingKeys) > 0 {
return "", fmt.Errorf("missing value for %s", strings.Join(missingKeys.List(), ", "))
}
return expanded, nil
} }
func ExpandContainerCommandAndArgs(container *v1.Container, envs []EnvVar) (command []string, args []string) { func ExpandContainerCommandAndArgs(container *v1.Container, envs []EnvVar) (command []string, args []string) {
......
...@@ -145,19 +145,21 @@ func TestExpandVolumeMountsWithSubpath(t *testing.T) { ...@@ -145,19 +145,21 @@ func TestExpandVolumeMountsWithSubpath(t *testing.T) {
envs []EnvVar envs []EnvVar
expectedSubPath string expectedSubPath string
expectedMountPath string expectedMountPath string
expectedOk bool
}{ }{
{ {
name: "subpath with no expansion", name: "subpath with no expansion",
container: &v1.Container{ container: &v1.Container{
VolumeMounts: []v1.VolumeMount{{SubPath: "foo"}}, VolumeMounts: []v1.VolumeMount{{SubPathExpr: "foo"}},
}, },
expectedSubPath: "foo", expectedSubPath: "foo",
expectedMountPath: "", expectedMountPath: "",
expectedOk: true,
}, },
{ {
name: "volumes with expanded subpath", name: "volumes with expanded subpath",
container: &v1.Container{ container: &v1.Container{
VolumeMounts: []v1.VolumeMount{{SubPath: "foo/$(POD_NAME)"}}, VolumeMounts: []v1.VolumeMount{{SubPathExpr: "foo/$(POD_NAME)"}},
}, },
envs: []EnvVar{ envs: []EnvVar{
{ {
...@@ -167,11 +169,12 @@ func TestExpandVolumeMountsWithSubpath(t *testing.T) { ...@@ -167,11 +169,12 @@ func TestExpandVolumeMountsWithSubpath(t *testing.T) {
}, },
expectedSubPath: "foo/bar", expectedSubPath: "foo/bar",
expectedMountPath: "", expectedMountPath: "",
expectedOk: true,
}, },
{ {
name: "volumes expanded with empty subpath", name: "volumes expanded with empty subpath",
container: &v1.Container{ container: &v1.Container{
VolumeMounts: []v1.VolumeMount{{SubPath: ""}}, VolumeMounts: []v1.VolumeMount{{SubPathExpr: ""}},
}, },
envs: []EnvVar{ envs: []EnvVar{
{ {
...@@ -181,19 +184,21 @@ func TestExpandVolumeMountsWithSubpath(t *testing.T) { ...@@ -181,19 +184,21 @@ func TestExpandVolumeMountsWithSubpath(t *testing.T) {
}, },
expectedSubPath: "", expectedSubPath: "",
expectedMountPath: "", expectedMountPath: "",
expectedOk: true,
}, },
{ {
name: "volumes expanded with no envs subpath", name: "volumes expanded with no envs subpath",
container: &v1.Container{ container: &v1.Container{
VolumeMounts: []v1.VolumeMount{{SubPath: "/foo/$(POD_NAME)"}}, VolumeMounts: []v1.VolumeMount{{SubPathExpr: "/foo/$(POD_NAME)"}},
}, },
expectedSubPath: "/foo/$(POD_NAME)", expectedSubPath: "/foo/$(POD_NAME)",
expectedMountPath: "", expectedMountPath: "",
expectedOk: false,
}, },
{ {
name: "volumes expanded with leading environment variable", name: "volumes expanded with leading environment variable",
container: &v1.Container{ container: &v1.Container{
VolumeMounts: []v1.VolumeMount{{SubPath: "$(POD_NAME)/bar"}}, VolumeMounts: []v1.VolumeMount{{SubPathExpr: "$(POD_NAME)/bar"}},
}, },
envs: []EnvVar{ envs: []EnvVar{
{ {
...@@ -203,11 +208,12 @@ func TestExpandVolumeMountsWithSubpath(t *testing.T) { ...@@ -203,11 +208,12 @@ func TestExpandVolumeMountsWithSubpath(t *testing.T) {
}, },
expectedSubPath: "foo/bar", expectedSubPath: "foo/bar",
expectedMountPath: "", expectedMountPath: "",
expectedOk: true,
}, },
{ {
name: "volumes with volume and subpath", name: "volumes with volume and subpath",
container: &v1.Container{ container: &v1.Container{
VolumeMounts: []v1.VolumeMount{{MountPath: "/foo", SubPath: "$(POD_NAME)/bar"}}, VolumeMounts: []v1.VolumeMount{{MountPath: "/foo", SubPathExpr: "$(POD_NAME)/bar"}},
}, },
envs: []EnvVar{ envs: []EnvVar{
{ {
...@@ -217,6 +223,7 @@ func TestExpandVolumeMountsWithSubpath(t *testing.T) { ...@@ -217,6 +223,7 @@ func TestExpandVolumeMountsWithSubpath(t *testing.T) {
}, },
expectedSubPath: "foo/bar", expectedSubPath: "foo/bar",
expectedMountPath: "/foo", expectedMountPath: "/foo",
expectedOk: true,
}, },
{ {
name: "volumes with volume and no subpath", name: "volumes with volume and no subpath",
...@@ -231,11 +238,78 @@ func TestExpandVolumeMountsWithSubpath(t *testing.T) { ...@@ -231,11 +238,78 @@ func TestExpandVolumeMountsWithSubpath(t *testing.T) {
}, },
expectedSubPath: "", expectedSubPath: "",
expectedMountPath: "/foo", expectedMountPath: "/foo",
expectedOk: true,
},
{
name: "subpaths with empty environment variable",
container: &v1.Container{
VolumeMounts: []v1.VolumeMount{{SubPathExpr: "foo/$(POD_NAME)/$(ANNOTATION)"}},
},
envs: []EnvVar{
{
Name: "ANNOTATION",
Value: "",
},
},
expectedSubPath: "foo/$(POD_NAME)/$(ANNOTATION)",
expectedMountPath: "",
expectedOk: false,
},
{
name: "subpaths with missing env variables",
container: &v1.Container{
VolumeMounts: []v1.VolumeMount{{SubPathExpr: "foo/$(ODD_NAME)/$(POD_NAME)"}},
},
envs: []EnvVar{
{
Name: "ODD_NAME",
Value: "bar",
},
},
expectedSubPath: "foo/$(ODD_NAME)/$(POD_NAME)",
expectedMountPath: "",
expectedOk: false,
},
{
name: "subpaths with empty expansion",
container: &v1.Container{
VolumeMounts: []v1.VolumeMount{{SubPathExpr: "$()"}},
},
expectedSubPath: "$()",
expectedMountPath: "",
expectedOk: false,
},
{
name: "subpaths with nested expandable envs",
container: &v1.Container{
VolumeMounts: []v1.VolumeMount{{SubPathExpr: "$(POD_NAME$(ANNOTATION))"}},
},
envs: []EnvVar{
{
Name: "POD_NAME",
Value: "foo",
},
{
Name: "ANNOTATION",
Value: "bar",
},
},
expectedSubPath: "$(POD_NAME$(ANNOTATION))",
expectedMountPath: "",
expectedOk: false,
}, },
} }
for _, tc := range cases { for _, tc := range cases {
actualSubPath := ExpandContainerVolumeMounts(tc.container.VolumeMounts[0], tc.envs) actualSubPath, err := ExpandContainerVolumeMounts(tc.container.VolumeMounts[0], tc.envs)
ok := err == nil
if e, a := tc.expectedOk, ok; !reflect.DeepEqual(e, a) {
t.Errorf("%v: unexpected validation failure of subpath; expected %v, got %v", tc.name, e, a)
}
if !ok {
// if ExpandContainerVolumeMounts returns an error, we don't care what the actualSubPath value is
continue
}
if e, a := tc.expectedSubPath, actualSubPath; !reflect.DeepEqual(e, a) { if e, a := tc.expectedSubPath, actualSubPath; !reflect.DeepEqual(e, a) {
t.Errorf("%v: unexpected subpath; expected %v, got %v", tc.name, e, a) t.Errorf("%v: unexpected subpath; expected %v, got %v", tc.name, e, a)
} }
......
...@@ -159,27 +159,40 @@ func makeMounts(pod *v1.Pod, podDir string, container *v1.Container, hostName, h ...@@ -159,27 +159,40 @@ func makeMounts(pod *v1.Pod, podDir string, container *v1.Container, hostName, h
if err != nil { if err != nil {
return nil, cleanupAction, err return nil, cleanupAction, err
} }
if mount.SubPath != "" {
subPath := mount.SubPath
if mount.SubPathExpr != "" {
if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeSubpath) { if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeSubpath) {
return nil, cleanupAction, fmt.Errorf("volume subpaths are disabled") return nil, cleanupAction, fmt.Errorf("volume subpaths are disabled")
} }
// Expand subpath variables if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeSubpathEnvExpansion) {
if utilfeature.DefaultFeatureGate.Enabled(features.VolumeSubpathEnvExpansion) { return nil, cleanupAction, fmt.Errorf("volume subpath expansion is disabled")
mount.SubPath = kubecontainer.ExpandContainerVolumeMounts(mount, expandEnvs) }
subPath, err = kubecontainer.ExpandContainerVolumeMounts(mount, expandEnvs)
if err != nil {
return nil, cleanupAction, err
}
}
if subPath != "" {
if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeSubpath) {
return nil, cleanupAction, fmt.Errorf("volume subpaths are disabled")
} }
if filepath.IsAbs(mount.SubPath) { if filepath.IsAbs(subPath) {
return nil, cleanupAction, fmt.Errorf("error SubPath `%s` must not be an absolute path", mount.SubPath) return nil, cleanupAction, fmt.Errorf("error SubPath `%s` must not be an absolute path", subPath)
} }
err = volumevalidation.ValidatePathNoBacksteps(mount.SubPath) err = volumevalidation.ValidatePathNoBacksteps(subPath)
if err != nil { if err != nil {
return nil, cleanupAction, fmt.Errorf("unable to provision SubPath `%s`: %v", mount.SubPath, err) return nil, cleanupAction, fmt.Errorf("unable to provision SubPath `%s`: %v", subPath, err)
} }
volumePath := hostPath volumePath := hostPath
hostPath = filepath.Join(volumePath, mount.SubPath) hostPath = filepath.Join(volumePath, subPath)
if subPathExists, err := mounter.ExistsPath(hostPath); err != nil { if subPathExists, err := mounter.ExistsPath(hostPath); err != nil {
klog.Errorf("Could not determine if subPath %s exists; will not attempt to change its permissions", hostPath) klog.Errorf("Could not determine if subPath %s exists; will not attempt to change its permissions", hostPath)
...@@ -193,7 +206,7 @@ func makeMounts(pod *v1.Pod, podDir string, container *v1.Container, hostName, h ...@@ -193,7 +206,7 @@ func makeMounts(pod *v1.Pod, podDir string, container *v1.Container, hostName, h
if err != nil { if err != nil {
return nil, cleanupAction, err return nil, cleanupAction, err
} }
if err := mounter.SafeMakeDir(mount.SubPath, volumePath, perm); err != nil { if err := mounter.SafeMakeDir(subPath, volumePath, perm); err != nil {
// Don't pass detailed error back to the user because it could give information about host filesystem // Don't pass detailed error back to the user because it could give information about host filesystem
klog.Errorf("failed to create subPath directory for volumeMount %q of container %q: %v", mount.Name, container.Name, err) klog.Errorf("failed to create subPath directory for volumeMount %q of container %q: %v", mount.Name, container.Name, err)
return nil, cleanupAction, fmt.Errorf("failed to create subPath directory for volumeMount %q of container %q", mount.Name, container.Name) return nil, cleanupAction, fmt.Errorf("failed to create subPath directory for volumeMount %q of container %q", mount.Name, container.Name)
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -4604,6 +4604,14 @@ message VolumeMount { ...@@ -4604,6 +4604,14 @@ message VolumeMount {
// This field is beta in 1.10. // This field is beta in 1.10.
// +optional // +optional
optional string mountPropagation = 5; optional string mountPropagation = 5;
// Expanded path within the volume from which the container's volume should be mounted.
// Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment.
// Defaults to "" (volume's root).
// SubPathExpr and SubPath are mutually exclusive.
// This field is alpha in 1.14.
// +optional
optional string subPathExpr = 6;
} }
// VolumeNodeAffinity defines constraints that limit what nodes this volume can be accessed from. // VolumeNodeAffinity defines constraints that limit what nodes this volume can be accessed from.
......
...@@ -1737,6 +1737,13 @@ type VolumeMount struct { ...@@ -1737,6 +1737,13 @@ type VolumeMount struct {
// This field is beta in 1.10. // This field is beta in 1.10.
// +optional // +optional
MountPropagation *MountPropagationMode `json:"mountPropagation,omitempty" protobuf:"bytes,5,opt,name=mountPropagation,casttype=MountPropagationMode"` MountPropagation *MountPropagationMode `json:"mountPropagation,omitempty" protobuf:"bytes,5,opt,name=mountPropagation,casttype=MountPropagationMode"`
// Expanded path within the volume from which the container's volume should be mounted.
// Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment.
// Defaults to "" (volume's root).
// SubPathExpr and SubPath are mutually exclusive.
// This field is alpha in 1.14.
// +optional
SubPathExpr string `json:"subPathExpr,omitempty" protobuf:"bytes,6,opt,name=subPathExpr"`
} }
// MountPropagationMode describes mount propagation. // MountPropagationMode describes mount propagation.
......
...@@ -2260,6 +2260,7 @@ var map_VolumeMount = map[string]string{ ...@@ -2260,6 +2260,7 @@ var map_VolumeMount = map[string]string{
"mountPath": "Path within the container at which the volume should be mounted. Must not contain ':'.", "mountPath": "Path within the container at which the volume should be mounted. Must not contain ':'.",
"subPath": "Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).", "subPath": "Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).",
"mountPropagation": "mountPropagation determines how mounts are propagated from the host to container and the other way around. When not set, MountPropagationNone is used. This field is beta in 1.10.", "mountPropagation": "mountPropagation determines how mounts are propagated from the host to container and the other way around. When not set, MountPropagationNone is used. This field is beta in 1.10.",
"subPathExpr": "Expanded path within the volume from which the container's volume should be mounted. Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. Defaults to \"\" (volume's root). SubPathExpr and SubPath are mutually exclusive. This field is alpha in 1.14.",
} }
func (VolumeMount) SwaggerDoc() map[string]string { func (VolumeMount) SwaggerDoc() map[string]string {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment