diff --git a/pkg/api/types.go b/pkg/api/types.go index 9e1e3aac03c..a4e0a44165e 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1445,8 +1445,34 @@ type VolumeMount struct { // Defaults to "" (volume's root). // +optional SubPath string + // mountPropagation determines how mounts are propagated from the host + // to container and the other way around. + // When not set, MountPropagationHostToContainer is used. + // This field is alpha in 1.8 and can be reworked or removed in a future + // release. + // +optional + MountPropagation *MountPropagationMode } +// MountPropagationMode describes mount propagation. +type MountPropagationMode string + +const ( + // MountPropagationHostToContainer means that the volume in a container will + // receive new mounts from the host or other containers, but filesystems + // mounted inside the container won't be propagated to the host or other + // containers. + // Note that this mode is recursively applied to all mounts in the volume + // ("rslave" in Linux terminology). + MountPropagationHostToContainer MountPropagationMode = "HostToContainer" + // MountPropagationBidirectional means that the volume in a container will + // receive new mounts from the host or other containers, and its own mounts + // will be propagated from the container to the host or other containers. + // Note that this mode is recursively applied to all mounts in the volume + // ("rshared" in Linux terminology). + MountPropagationBidirectional MountPropagationMode = "Bidirectional" +) + // EnvVar represents an environment variable present in a Container. type EnvVar struct { // Required: This must be a C_IDENTIFIER. diff --git a/pkg/api/v1/zz_generated.conversion.go b/pkg/api/v1/zz_generated.conversion.go index b9671f69fd8..e1f6e94ed65 100644 --- a/pkg/api/v1/zz_generated.conversion.go +++ b/pkg/api/v1/zz_generated.conversion.go @@ -5133,6 +5133,7 @@ func autoConvert_v1_VolumeMount_To_api_VolumeMount(in *v1.VolumeMount, out *api. out.ReadOnly = in.ReadOnly out.MountPath = in.MountPath out.SubPath = in.SubPath + out.MountPropagation = (*api.MountPropagationMode)(unsafe.Pointer(in.MountPropagation)) return nil } @@ -5146,6 +5147,7 @@ func autoConvert_api_VolumeMount_To_v1_VolumeMount(in *api.VolumeMount, out *v1. out.ReadOnly = in.ReadOnly out.MountPath = in.MountPath out.SubPath = in.SubPath + out.MountPropagation = (*v1.MountPropagationMode)(unsafe.Pointer(in.MountPropagation)) return nil } diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index be6d5ad768e..e0c019a9cf5 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -1027,6 +1027,38 @@ func validatePathNoBacksteps(targetPath string, fldPath *field.Path) field.Error return allErrs } +// validateMountPropagation verifies that MountPropagation field is valid and +// allowed for given container. +func validateMountPropagation(mountPropagation *api.MountPropagationMode, container *api.Container, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if mountPropagation == nil { + return allErrs + } + if !utilfeature.DefaultFeatureGate.Enabled(features.MountPropagation) { + allErrs = append(allErrs, field.Forbidden(fldPath, "mount propagation is disabled by feature-gate")) + return allErrs + } + + supportedMountPropagations := sets.NewString(string(api.MountPropagationBidirectional), string(api.MountPropagationHostToContainer)) + if !supportedMountPropagations.Has(string(*mountPropagation)) { + allErrs = append(allErrs, field.NotSupported(fldPath, *mountPropagation, supportedMountPropagations.List())) + } + + if container == nil { + // The container is not available yet, e.g. during validation of + // PodPreset. Stop validation now, Pod validation will refuse final + // Pods with Bidirectional propagation in non-privileged containers. + return allErrs + } + + privileged := container.SecurityContext != nil && container.SecurityContext.Privileged != nil && *container.SecurityContext.Privileged + if *mountPropagation == api.MountPropagationBidirectional && !privileged { + allErrs = append(allErrs, field.Forbidden(fldPath, "Bidirectional mount propagation is available only to privileged containers")) + } + return allErrs +} + // This validate will make sure targetPath: // 1. is not abs path // 2. does not contain any '..' elements @@ -1845,7 +1877,7 @@ func validateSecretKeySelector(s *api.SecretKeySelector, fldPath *field.Path) fi return allErrs } -func ValidateVolumeMounts(mounts []api.VolumeMount, volumes sets.String, fldPath *field.Path) field.ErrorList { +func ValidateVolumeMounts(mounts []api.VolumeMount, volumes sets.String, container *api.Container, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} mountpoints := sets.NewString() @@ -1869,6 +1901,10 @@ func ValidateVolumeMounts(mounts []api.VolumeMount, volumes sets.String, fldPath if len(mnt.SubPath) > 0 { allErrs = append(allErrs, validateLocalDescendingPath(mnt.SubPath, fldPath.Child("subPath"))...) } + + if mnt.MountPropagation != nil { + allErrs = append(allErrs, validateMountPropagation(mnt.MountPropagation, container, fldPath.Child("mountPropagation"))...) + } } return allErrs } @@ -2135,7 +2171,7 @@ func validateContainers(containers []api.Container, volumes sets.String, fldPath allErrs = append(allErrs, validateContainerPorts(ctr.Ports, idxPath.Child("ports"))...) allErrs = append(allErrs, ValidateEnv(ctr.Env, idxPath.Child("env"))...) allErrs = append(allErrs, ValidateEnvFrom(ctr.EnvFrom, idxPath.Child("envFrom"))...) - allErrs = append(allErrs, ValidateVolumeMounts(ctr.VolumeMounts, volumes, idxPath.Child("volumeMounts"))...) + allErrs = append(allErrs, ValidateVolumeMounts(ctr.VolumeMounts, volumes, &ctr, idxPath.Child("volumeMounts"))...) allErrs = append(allErrs, validatePullPolicy(ctr.ImagePullPolicy, idxPath.Child("imagePullPolicy"))...) allErrs = append(allErrs, ValidateResourceRequirements(&ctr.Resources, idxPath.Child("resources"))...) allErrs = append(allErrs, ValidateSecurityContext(ctr.SecurityContext, idxPath.Child("securityContext"))...) diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 6d85afccf4a..24557e72e7b 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -3405,6 +3405,10 @@ func TestValidateEnvFrom(t *testing.T) { func TestValidateVolumeMounts(t *testing.T) { volumes := sets.NewString("abc", "123", "abc-123") + container := api.Container{ + SecurityContext: nil, + } + propagation := api.MountPropagationBidirectional successCase := []api.VolumeMount{ {Name: "abc", MountPath: "/foo"}, @@ -3415,28 +3419,154 @@ func TestValidateVolumeMounts(t *testing.T) { {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 { + if errs := ValidateVolumeMounts(successCase, volumes, &container, field.NewPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } errorCases := map[string][]api.VolumeMount{ - "empty name": {{Name: "", MountPath: "/foo"}}, - "name not found": {{Name: "", MountPath: "/foo"}}, - "empty mountpath": {{Name: "abc", MountPath: ""}}, - "relative mountpath": {{Name: "abc", MountPath: "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: "./.."}}, + "empty name": {{Name: "", MountPath: "/foo"}}, + "name not found": {{Name: "", MountPath: "/foo"}}, + "empty mountpath": {{Name: "abc", MountPath: ""}}, + "relative mountpath": {{Name: "abc", MountPath: "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: "./.."}}, + "disabled MountPropagation feature gate": {{Name: "abc", MountPath: "/bar", MountPropagation: &propagation}}, } for k, v := range errorCases { - if errs := ValidateVolumeMounts(v, volumes, field.NewPath("field")); len(errs) == 0 { + if errs := ValidateVolumeMounts(v, volumes, &container, field.NewPath("field")); len(errs) == 0 { t.Errorf("expected failure for %s", k) } } } +func TestValidateMountPropagation(t *testing.T) { + bTrue := true + bFalse := false + privilegedContainer := &api.Container{ + SecurityContext: &api.SecurityContext{ + Privileged: &bTrue, + }, + } + nonPrivilegedContainer := &api.Container{ + SecurityContext: &api.SecurityContext{ + Privileged: &bFalse, + }, + } + defaultContainer := &api.Container{} + + propagationBidirectional := api.MountPropagationBidirectional + propagationHostToContainer := api.MountPropagationHostToContainer + propagationInvalid := api.MountPropagationMode("invalid") + + tests := []struct { + mount api.VolumeMount + container *api.Container + expectError bool + }{ + { + // implicitly non-privileged container + no propagation + api.VolumeMount{Name: "foo", MountPath: "/foo"}, + defaultContainer, + false, + }, + { + // implicitly non-privileged container + HostToContainer + api.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationHostToContainer}, + defaultContainer, + false, + }, + { + // error: implicitly non-privileged container + Bidirectional + api.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationBidirectional}, + defaultContainer, + true, + }, + { + // explicitly non-privileged container + no propagation + api.VolumeMount{Name: "foo", MountPath: "/foo"}, + nonPrivilegedContainer, + false, + }, + { + // explicitly non-privileged container + HostToContainer + api.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationHostToContainer}, + nonPrivilegedContainer, + false, + }, + { + // explicitly non-privileged container + HostToContainer + api.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationBidirectional}, + nonPrivilegedContainer, + true, + }, + { + // privileged container + no propagation + api.VolumeMount{Name: "foo", MountPath: "/foo"}, + privilegedContainer, + false, + }, + { + // privileged container + HostToContainer + api.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationHostToContainer}, + privilegedContainer, + false, + }, + { + // privileged container + Bidirectional + api.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationBidirectional}, + privilegedContainer, + false, + }, + { + // error: privileged container + invalid mount propagation + api.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationInvalid}, + privilegedContainer, + true, + }, + { + // no container + Bidirectional + api.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationBidirectional}, + nil, + false, + }, + } + + // Enable MountPropagation for this test + priorityEnabled := utilfeature.DefaultFeatureGate.Enabled("MountPropagation") + defer func() { + var err error + // restoring the old value + if priorityEnabled { + err = utilfeature.DefaultFeatureGate.Set("MountPropagation=true") + } else { + err = utilfeature.DefaultFeatureGate.Set("MountPropagation=false") + } + if err != nil { + t.Errorf("Failed to restore feature gate for MountPropagation: %v", err) + } + }() + err := utilfeature.DefaultFeatureGate.Set("MountPropagation=true") + if err != nil { + t.Errorf("Failed to enable feature gate for MountPropagation: %v", err) + return + } + + for i, test := range tests { + volumes := sets.NewString("foo") + errs := ValidateVolumeMounts([]api.VolumeMount{test.mount}, volumes, test.container, field.NewPath("field")) + if test.expectError && len(errs) == 0 { + t.Errorf("test %d expected error, got none", i) + } + if !test.expectError && len(errs) != 0 { + t.Errorf("test %d expected success, got error: %v", i, errs) + } + } + +} + func TestValidateProbe(t *testing.T) { handler := api.Handler{Exec: &api.ExecAction{Command: []string{"echo"}}} // These fields must be positive. diff --git a/pkg/api/zz_generated.deepcopy.go b/pkg/api/zz_generated.deepcopy.go index 19fd743cc0c..d9ab39d3354 100644 --- a/pkg/api/zz_generated.deepcopy.go +++ b/pkg/api/zz_generated.deepcopy.go @@ -1409,7 +1409,9 @@ func (in *Container) DeepCopyInto(out *Container) { if in.VolumeMounts != nil { in, out := &in.VolumeMounts, &out.VolumeMounts *out = make([]VolumeMount, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.LivenessProbe != nil { in, out := &in.LivenessProbe, &out.LivenessProbe @@ -5931,6 +5933,15 @@ func (in *Volume) DeepCopy() *Volume { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VolumeMount) DeepCopyInto(out *VolumeMount) { *out = *in + if in.MountPropagation != nil { + in, out := &in.MountPropagation, &out.MountPropagation + if *in == nil { + *out = nil + } else { + *out = new(MountPropagationMode) + **out = **in + } + } return } diff --git a/pkg/apis/settings/validation/validation.go b/pkg/apis/settings/validation/validation.go index f38f07e7a81..6f02781ff49 100644 --- a/pkg/apis/settings/validation/validation.go +++ b/pkg/apis/settings/validation/validation.go @@ -47,7 +47,7 @@ func ValidatePodPresetSpec(spec *settings.PodPresetSpec, fldPath *field.Path) fi allErrs = append(allErrs, vErrs...) allErrs = append(allErrs, apivalidation.ValidateEnv(spec.Env, fldPath.Child("env"))...) allErrs = append(allErrs, apivalidation.ValidateEnvFrom(spec.EnvFrom, fldPath.Child("envFrom"))...) - allErrs = append(allErrs, apivalidation.ValidateVolumeMounts(spec.VolumeMounts, volumes, fldPath.Child("volumeMounts"))...) + allErrs = append(allErrs, apivalidation.ValidateVolumeMounts(spec.VolumeMounts, volumes, nil, fldPath.Child("volumeMounts"))...) return allErrs } diff --git a/pkg/apis/settings/zz_generated.deepcopy.go b/pkg/apis/settings/zz_generated.deepcopy.go index 5d13b4ab569..a6d188a3986 100644 --- a/pkg/apis/settings/zz_generated.deepcopy.go +++ b/pkg/apis/settings/zz_generated.deepcopy.go @@ -142,7 +142,9 @@ func (in *PodPresetSpec) DeepCopyInto(out *PodPresetSpec) { if in.VolumeMounts != nil { in, out := &in.VolumeMounts, &out.VolumeMounts *out = make([]api.VolumeMount, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index 840882dce6d..c60b05effd1 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -1547,8 +1547,34 @@ type VolumeMount struct { // Defaults to "" (volume's root). // +optional SubPath string `json:"subPath,omitempty" protobuf:"bytes,4,opt,name=subPath"` + // mountPropagation determines how mounts are propagated from the host + // to container and the other way around. + // When not set, MountPropagationHostToContainer is used. + // This field is alpha in 1.8 and can be reworked or removed in a future + // release. + // +optional + MountPropagation *MountPropagationMode `json:"mountPropagation,omitempty"` } +// MountPropagationMode describes mount propagation. +type MountPropagationMode string + +const ( + // MountPropagationHostToContainer means that the volume in a container will + // receive new mounts from the host or other containers, but filesystems + // mounted inside the container won't be propagated to the host or other + // containers. + // Note that this mode is recursively applied to all mounts in the volume + // ("rslave" in Linux terminology). + MountPropagationHostToContainer MountPropagationMode = "HostToContainer" + // MountPropagationBidirectional means that the volume in a container will + // receive new mounts from the host or other containers, and its own mounts + // will be propagated from the container to the host or other containers. + // Note that this mode is recursively applied to all mounts in the volume + // ("rshared" in Linux terminology). + MountPropagationBidirectional MountPropagationMode = "Bidirectional" +) + // EnvVar represents an environment variable present in a Container. type EnvVar struct { // Name of the environment variable. Must be a C_IDENTIFIER. diff --git a/staging/src/k8s.io/api/core/v1/zz_generated.deepcopy.go b/staging/src/k8s.io/api/core/v1/zz_generated.deepcopy.go index 517d9f00887..8133d135b27 100644 --- a/staging/src/k8s.io/api/core/v1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/api/core/v1/zz_generated.deepcopy.go @@ -1409,7 +1409,9 @@ func (in *Container) DeepCopyInto(out *Container) { if in.VolumeMounts != nil { in, out := &in.VolumeMounts, &out.VolumeMounts *out = make([]VolumeMount, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.LivenessProbe != nil { in, out := &in.LivenessProbe, &out.LivenessProbe @@ -5933,6 +5935,15 @@ func (in *Volume) DeepCopy() *Volume { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VolumeMount) DeepCopyInto(out *VolumeMount) { *out = *in + if in.MountPropagation != nil { + in, out := &in.MountPropagation, &out.MountPropagation + if *in == nil { + *out = nil + } else { + *out = new(MountPropagationMode) + **out = **in + } + } return } diff --git a/staging/src/k8s.io/api/settings/v1alpha1/zz_generated.deepcopy.go b/staging/src/k8s.io/api/settings/v1alpha1/zz_generated.deepcopy.go index a44bd2fd29d..07e6ab72d0e 100644 --- a/staging/src/k8s.io/api/settings/v1alpha1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/api/settings/v1alpha1/zz_generated.deepcopy.go @@ -142,7 +142,9 @@ func (in *PodPresetSpec) DeepCopyInto(out *PodPresetSpec) { if in.VolumeMounts != nil { in, out := &in.VolumeMounts, &out.VolumeMounts *out = make([]v1.VolumeMount, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return }