Merge pull request #126806 from carlory/fix-image-volume-mount

Kubelet should honour the VolumeAttributes which are reported by the volume plugin
This commit is contained in:
Kubernetes Prow Robot 2024-11-05 23:21:35 +00:00 committed by GitHub
commit b5e6456795
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 308 additions and 97 deletions

View File

@ -277,18 +277,6 @@ func makeMounts(pod *v1.Pod, podDir string, container *v1.Container, hostName, h
mounts := []kubecontainer.Mount{} mounts := []kubecontainer.Mount{}
var cleanupAction func() var cleanupAction func()
for i, mount := range container.VolumeMounts { for i, mount := range container.VolumeMounts {
// Check if the mount is referencing an OCI volume
if imageVolumes != nil && utilfeature.DefaultFeatureGate.Enabled(features.ImageVolume) {
if image, ok := imageVolumes[mount.Name]; ok {
mounts = append(mounts, kubecontainer.Mount{
Name: mount.Name,
ContainerPath: mount.MountPath,
Image: image,
})
continue
}
}
// do not mount /etc/hosts if container is already mounting on the path // do not mount /etc/hosts if container is already mounting on the path
mountEtcHostsFile = mountEtcHostsFile && (mount.MountPath != etcHostsPath) mountEtcHostsFile = mountEtcHostsFile && (mount.MountPath != etcHostsPath)
vol, ok := podVolumes[mount.Name] vol, ok := podVolumes[mount.Name]
@ -306,69 +294,82 @@ func makeMounts(pod *v1.Pod, podDir string, container *v1.Container, hostName, h
vol.SELinuxLabeled = true vol.SELinuxLabeled = true
relabelVolume = true relabelVolume = true
} }
hostPath, err := volumeutil.GetPath(vol.Mounter)
if err != nil { var (
return nil, cleanupAction, err hostPath string
image *runtimeapi.ImageSpec
err error
)
if imageVolumes != nil && utilfeature.DefaultFeatureGate.Enabled(features.ImageVolume) {
image = imageVolumes[mount.Name]
} }
subPath := mount.SubPath if image == nil {
if mount.SubPathExpr != "" { hostPath, err = volumeutil.GetPath(vol.Mounter)
subPath, err = kubecontainer.ExpandContainerVolumeMounts(mount, expandEnvs)
if err != nil { if err != nil {
return nil, cleanupAction, err return nil, cleanupAction, err
} }
}
if subPath != "" { subPath := mount.SubPath
if utilfs.IsAbs(subPath) { if mount.SubPathExpr != "" {
return nil, cleanupAction, fmt.Errorf("error SubPath `%s` must not be an absolute path", subPath) subPath, err = kubecontainer.ExpandContainerVolumeMounts(mount, expandEnvs)
}
err = volumevalidation.ValidatePathNoBacksteps(subPath)
if err != nil {
return nil, cleanupAction, fmt.Errorf("unable to provision SubPath `%s`: %v", subPath, err)
}
volumePath := hostPath
hostPath = filepath.Join(volumePath, subPath)
if subPathExists, err := hu.PathExists(hostPath); err != nil {
klog.ErrorS(nil, "Could not determine if subPath exists, will not attempt to change its permissions", "path", hostPath)
} else if !subPathExists {
// Create the sub path now because if it's auto-created later when referenced, it may have an
// incorrect ownership and mode. For example, the sub path directory must have at least g+rwx
// when the pod specifies an fsGroup, and if the directory is not created here, Docker will
// later auto-create it with the incorrect mode 0750
// Make extra care not to escape the volume!
perm, err := hu.GetMode(volumePath)
if err != nil { if err != nil {
return nil, cleanupAction, err return nil, cleanupAction, err
} }
if err := subpather.SafeMakeDir(subPath, volumePath, perm); err != nil { }
if subPath != "" {
if utilfs.IsAbs(subPath) {
return nil, cleanupAction, fmt.Errorf("error SubPath `%s` must not be an absolute path", subPath)
}
err = volumevalidation.ValidatePathNoBacksteps(subPath)
if err != nil {
return nil, cleanupAction, fmt.Errorf("unable to provision SubPath `%s`: %w", subPath, err)
}
volumePath := hostPath
hostPath = filepath.Join(volumePath, subPath)
if subPathExists, err := hu.PathExists(hostPath); err != nil {
klog.ErrorS(nil, "Could not determine if subPath exists, will not attempt to change its permissions", "path", hostPath)
} else if !subPathExists {
// Create the sub path now because if it's auto-created later when referenced, it may have an
// incorrect ownership and mode. For example, the sub path directory must have at least g+rwx
// when the pod specifies an fsGroup, and if the directory is not created here, Docker will
// later auto-create it with the incorrect mode 0750
// Make extra care not to escape the volume!
perm, err := hu.GetMode(volumePath)
if err != nil {
return nil, cleanupAction, err
}
if err := subpather.SafeMakeDir(subPath, volumePath, perm); err != nil {
// Don't pass detailed error back to the user because it could give information about host filesystem
klog.ErrorS(err, "Failed to create subPath directory for volumeMount of the container", "containerName", container.Name, "volumeMountName", mount.Name)
return nil, cleanupAction, fmt.Errorf("failed to create subPath directory for volumeMount %q of container %q", mount.Name, container.Name)
}
}
hostPath, cleanupAction, err = subpather.PrepareSafeSubpath(subpath.Subpath{
VolumeMountIndex: i,
Path: hostPath,
VolumeName: vol.InnerVolumeSpecName,
VolumePath: volumePath,
PodDir: podDir,
ContainerName: container.Name,
})
if 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.ErrorS(err, "Failed to create subPath directory for volumeMount of the container", "containerName", container.Name, "volumeMountName", mount.Name) klog.ErrorS(err, "Failed to prepare subPath for volumeMount of the container", "containerName", container.Name, "volumeMountName", mount.Name)
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 prepare subPath for volumeMount %q of container %q", mount.Name, container.Name)
} }
} }
hostPath, cleanupAction, err = subpather.PrepareSafeSubpath(subpath.Subpath{
VolumeMountIndex: i,
Path: hostPath,
VolumeName: vol.InnerVolumeSpecName,
VolumePath: volumePath,
PodDir: podDir,
ContainerName: container.Name,
})
if err != nil {
// Don't pass detailed error back to the user because it could give information about host filesystem
klog.ErrorS(err, "Failed to prepare subPath for volumeMount of the container", "containerName", container.Name, "volumeMountName", mount.Name)
return nil, cleanupAction, fmt.Errorf("failed to prepare subPath for volumeMount %q of container %q", mount.Name, container.Name)
}
}
// Docker Volume Mounts fail on Windows if it is not of the form C:/ // Docker Volume Mounts fail on Windows if it is not of the form C:/
if volumeutil.IsWindowsLocalPath(runtime.GOOS, hostPath) { if hostPath != "" && volumeutil.IsWindowsLocalPath(runtime.GOOS, hostPath) {
hostPath = volumeutil.MakeAbsolutePath(runtime.GOOS, hostPath) hostPath = volumeutil.MakeAbsolutePath(runtime.GOOS, hostPath)
}
} }
containerPath := mount.MountPath containerPath := mount.MountPath
@ -396,6 +397,7 @@ func makeMounts(pod *v1.Pod, podDir string, container *v1.Container, hostName, h
Name: mount.Name, Name: mount.Name,
ContainerPath: containerPath, ContainerPath: containerPath,
HostPath: hostPath, HostPath: hostPath,
Image: image,
ReadOnly: mount.ReadOnly || mustMountRO, ReadOnly: mount.ReadOnly || mustMountRO,
RecursiveReadOnly: rro, RecursiveReadOnly: rro,
SELinuxRelabel: relabelVolume, SELinuxRelabel: relabelVolume,

View File

@ -20,16 +20,21 @@ limitations under the License.
package kubelet package kubelet
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/utils/ptr" "k8s.io/utils/ptr"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1" runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
_ "k8s.io/kubernetes/pkg/apis/core/install" _ "k8s.io/kubernetes/pkg/apis/core/install"
"k8s.io/kubernetes/pkg/features"
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
"k8s.io/kubernetes/pkg/volume"
volumetest "k8s.io/kubernetes/pkg/volume/testing" volumetest "k8s.io/kubernetes/pkg/volume/testing"
"k8s.io/kubernetes/pkg/volume/util/hostutil" "k8s.io/kubernetes/pkg/volume/util/hostutil"
"k8s.io/kubernetes/pkg/volume/util/subpath" "k8s.io/kubernetes/pkg/volume/util/subpath"
@ -40,16 +45,21 @@ func TestMakeMounts(t *testing.T) {
propagationHostToContainer := v1.MountPropagationHostToContainer propagationHostToContainer := v1.MountPropagationHostToContainer
propagationBidirectional := v1.MountPropagationBidirectional propagationBidirectional := v1.MountPropagationBidirectional
propagationNone := v1.MountPropagationNone propagationNone := v1.MountPropagationNone
image1 := &runtimeapi.ImageSpec{Image: "image1"}
image2 := &runtimeapi.ImageSpec{Image: "image2"}
testCases := map[string]struct { testCases := map[string]struct {
container v1.Container container v1.Container
podVolumes kubecontainer.VolumeMap podVolumes kubecontainer.VolumeMap
supportsRRO bool imageVolumes kubecontainer.ImageVolumes
expectErr bool imageVolumeFeatureEnabled []bool
expectedErrMsg string supportsRRO bool
expectedMounts []kubecontainer.Mount expectErr bool
expectedErrMsg string
expectedMounts []kubecontainer.Mount
}{ }{
"valid mounts in unprivileged container": { "valid mounts in unprivileged container": { // TODO: remove it once image volume feature is GA
imageVolumeFeatureEnabled: []bool{true, false},
podVolumes: kubecontainer.VolumeMap{ podVolumes: kubecontainer.VolumeMap{
"disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}}, "disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}},
"disk4": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/host"}}, "disk4": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/host"}},
@ -118,7 +128,8 @@ func TestMakeMounts(t *testing.T) {
}, },
expectErr: false, expectErr: false,
}, },
"valid mounts in privileged container": { "valid mounts in privileged container": { // TODO: remove it once image volume feature is GA
imageVolumeFeatureEnabled: []bool{true, false},
podVolumes: kubecontainer.VolumeMap{ podVolumes: kubecontainer.VolumeMap{
"disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}}, "disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}},
"disk4": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/host"}}, "disk4": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/host"}},
@ -177,7 +188,198 @@ func TestMakeMounts(t *testing.T) {
}, },
expectErr: false, expectErr: false,
}, },
"valid mounts in unprivileged container with image volumes": {
imageVolumeFeatureEnabled: []bool{true},
podVolumes: kubecontainer.VolumeMap{
"disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}},
"disk4": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/host"}},
"disk5": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/var/lib/kubelet/podID/volumes/empty/disk5"}},
"image1": kubecontainer.VolumeInfo{Mounter: &stubVolume{attributes: volume.Attributes{ReadOnly: true}}},
"image2": kubecontainer.VolumeInfo{Mounter: &stubVolume{attributes: volume.Attributes{ReadOnly: true}}},
},
imageVolumes: kubecontainer.ImageVolumes{
"image1": image1,
"image2": image2,
},
container: v1.Container{
Name: "container1",
VolumeMounts: []v1.VolumeMount{
{
MountPath: "/etc/hosts",
Name: "disk",
ReadOnly: false,
MountPropagation: &propagationHostToContainer,
},
{
MountPath: "/mnt/path3",
Name: "disk",
ReadOnly: true,
MountPropagation: &propagationNone,
},
{
MountPath: "/mnt/path4",
Name: "disk4",
ReadOnly: false,
},
{
MountPath: "/mnt/path5",
Name: "disk5",
ReadOnly: false,
},
{
MountPath: "/mnt/image1",
Name: "image1",
ReadOnly: false,
},
{
MountPath: "/mnt/image2",
Name: "image2",
ReadOnly: true,
},
},
},
expectedMounts: []kubecontainer.Mount{
{
Name: "disk",
ContainerPath: "/etc/hosts",
HostPath: "/mnt/disk",
ReadOnly: false,
SELinuxRelabel: false,
Propagation: runtimeapi.MountPropagation_PROPAGATION_HOST_TO_CONTAINER,
},
{
Name: "disk",
ContainerPath: "/mnt/path3",
HostPath: "/mnt/disk",
ReadOnly: true,
SELinuxRelabel: false,
Propagation: runtimeapi.MountPropagation_PROPAGATION_PRIVATE,
},
{
Name: "disk4",
ContainerPath: "/mnt/path4",
HostPath: "/mnt/host",
ReadOnly: false,
SELinuxRelabel: false,
Propagation: runtimeapi.MountPropagation_PROPAGATION_PRIVATE,
},
{
Name: "disk5",
ContainerPath: "/mnt/path5",
HostPath: "/var/lib/kubelet/podID/volumes/empty/disk5",
ReadOnly: false,
SELinuxRelabel: false,
Propagation: runtimeapi.MountPropagation_PROPAGATION_PRIVATE,
},
{
Name: "image1",
ContainerPath: "/mnt/image1",
Image: image1,
ReadOnly: true,
SELinuxRelabel: false,
},
{
Name: "image2",
ContainerPath: "/mnt/image2",
Image: image2,
ReadOnly: true,
SELinuxRelabel: false,
},
},
expectErr: false,
},
"valid mounts in privileged container with image volumes": {
imageVolumeFeatureEnabled: []bool{true},
podVolumes: kubecontainer.VolumeMap{
"disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}},
"disk4": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/host"}},
"disk5": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/var/lib/kubelet/podID/volumes/empty/disk5"}},
"image1": kubecontainer.VolumeInfo{Mounter: &stubVolume{attributes: volume.Attributes{ReadOnly: true}}},
"image2": kubecontainer.VolumeInfo{Mounter: &stubVolume{attributes: volume.Attributes{ReadOnly: true}}},
},
imageVolumes: kubecontainer.ImageVolumes{
"image1": image1,
"image2": image2,
},
container: v1.Container{
Name: "container1",
VolumeMounts: []v1.VolumeMount{
{
MountPath: "/etc/hosts",
Name: "disk",
ReadOnly: false,
MountPropagation: &propagationBidirectional,
},
{
MountPath: "/mnt/path3",
Name: "disk",
ReadOnly: true,
MountPropagation: &propagationHostToContainer,
},
{
MountPath: "/mnt/path4",
Name: "disk4",
ReadOnly: false,
},
{
MountPath: "/mnt/image1",
Name: "image1",
ReadOnly: false,
},
{
MountPath: "/mnt/image2",
Name: "image2",
ReadOnly: true,
},
},
SecurityContext: &v1.SecurityContext{
Privileged: &bTrue,
},
},
expectedMounts: []kubecontainer.Mount{
{
Name: "disk",
ContainerPath: "/etc/hosts",
HostPath: "/mnt/disk",
ReadOnly: false,
SELinuxRelabel: false,
Propagation: runtimeapi.MountPropagation_PROPAGATION_BIDIRECTIONAL,
},
{
Name: "disk",
ContainerPath: "/mnt/path3",
HostPath: "/mnt/disk",
ReadOnly: true,
SELinuxRelabel: false,
Propagation: runtimeapi.MountPropagation_PROPAGATION_HOST_TO_CONTAINER,
},
{
Name: "disk4",
ContainerPath: "/mnt/path4",
HostPath: "/mnt/host",
ReadOnly: false,
SELinuxRelabel: false,
Propagation: runtimeapi.MountPropagation_PROPAGATION_PRIVATE,
},
{
Name: "image1",
ContainerPath: "/mnt/image1",
Image: image1,
ReadOnly: true,
SELinuxRelabel: false,
},
{
Name: "image2",
ContainerPath: "/mnt/image2",
Image: image2,
ReadOnly: true,
SELinuxRelabel: false,
},
},
expectErr: false,
},
"invalid absolute SubPath": { "invalid absolute SubPath": {
imageVolumeFeatureEnabled: []bool{true, false},
podVolumes: kubecontainer.VolumeMap{ podVolumes: kubecontainer.VolumeMap{
"disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}}, "disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}},
}, },
@ -195,6 +397,7 @@ func TestMakeMounts(t *testing.T) {
expectedErrMsg: "error SubPath `/must/not/be/absolute` must not be an absolute path", expectedErrMsg: "error SubPath `/must/not/be/absolute` must not be an absolute path",
}, },
"invalid SubPath with backsteps": { "invalid SubPath with backsteps": {
imageVolumeFeatureEnabled: []bool{true, false},
podVolumes: kubecontainer.VolumeMap{ podVolumes: kubecontainer.VolumeMap{
"disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}}, "disk": kubecontainer.VolumeInfo{Mounter: &stubVolume{path: "/mnt/disk"}},
}, },
@ -212,7 +415,7 @@ func TestMakeMounts(t *testing.T) {
expectedErrMsg: "unable to provision SubPath `no/backsteps/../allowed`: must not contain '..'", expectedErrMsg: "unable to provision SubPath `no/backsteps/../allowed`: must not contain '..'",
}, },
"volume doesn't exist": { "volume doesn't exist": {
podVolumes: kubecontainer.VolumeMap{}, imageVolumeFeatureEnabled: []bool{true, false},
container: v1.Container{ container: v1.Container{
VolumeMounts: []v1.VolumeMount{ VolumeMounts: []v1.VolumeMount{
{ {
@ -226,6 +429,7 @@ func TestMakeMounts(t *testing.T) {
expectedErrMsg: "cannot find volume \"disk\" to mount into container \"\"", expectedErrMsg: "cannot find volume \"disk\" to mount into container \"\"",
}, },
"volume mounter is nil": { "volume mounter is nil": {
imageVolumeFeatureEnabled: []bool{true, false},
podVolumes: kubecontainer.VolumeMap{ podVolumes: kubecontainer.VolumeMap{
"disk": kubecontainer.VolumeInfo{}, "disk": kubecontainer.VolumeInfo{},
}, },
@ -244,32 +448,36 @@ func TestMakeMounts(t *testing.T) {
} }
for name, tc := range testCases { for name, tc := range testCases {
t.Run(name, func(t *testing.T) { for _, featureEnabled := range tc.imageVolumeFeatureEnabled {
fhu := hostutil.NewFakeHostUtil(nil) name := fmt.Sprintf("features.ImageVolume is %v, %s", featureEnabled, name)
fsp := &subpath.FakeSubpath{} t.Run(name, func(t *testing.T) {
pod := v1.Pod{ featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ImageVolume, featureEnabled)
Spec: v1.PodSpec{ fhu := hostutil.NewFakeHostUtil(nil)
HostNetwork: true, fsp := &subpath.FakeSubpath{}
}, pod := v1.Pod{
} Spec: v1.PodSpec{
HostNetwork: true,
mounts, _, err := makeMounts(&pod, "/pod", &tc.container, "fakepodname", "", []string{""}, tc.podVolumes, fhu, fsp, nil, tc.supportsRRO, nil) },
// validate only the error if we expect an error
if tc.expectErr {
if err == nil || err.Error() != tc.expectedErrMsg {
t.Fatalf("expected error message `%s` but got `%v`", tc.expectedErrMsg, err)
} }
return
}
// otherwise validate the mounts mounts, _, err := makeMounts(&pod, "/pod", &tc.container, "fakepodname", "", []string{""}, tc.podVolumes, fhu, fsp, nil, tc.supportsRRO, tc.imageVolumes)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, tc.expectedMounts, mounts, "mounts of container %+v", tc.container) // validate only the error if we expect an error
}) if tc.expectErr {
if err == nil || err.Error() != tc.expectedErrMsg {
t.Fatalf("expected error message `%s` but got `%v`", tc.expectedErrMsg, err)
}
return
}
// otherwise validate the mounts
if err != nil {
t.Fatal(err)
}
assert.Equal(t, tc.expectedMounts, mounts, "mounts of container %+v", tc.container)
})
}
} }
} }

View File

@ -689,7 +689,8 @@ func TestVolumeUnmountAndDetachControllerEnabled(t *testing.T) {
} }
type stubVolume struct { type stubVolume struct {
path string path string
attributes volume.Attributes
volume.MetricsNil volume.MetricsNil
} }
@ -698,7 +699,7 @@ func (f *stubVolume) GetPath() string {
} }
func (f *stubVolume) GetAttributes() volume.Attributes { func (f *stubVolume) GetAttributes() volume.Attributes {
return volume.Attributes{} return f.attributes
} }
func (f *stubVolume) SetUp(mounterArgs volume.MounterArgs) error { func (f *stubVolume) SetUp(mounterArgs volume.MounterArgs) error {

View File

@ -67,8 +67,8 @@ func (o *imagePlugin) ConstructVolumeSpec(volumeName, mountPath string) (volume.
func (o *imagePlugin) GetAttributes() volume.Attributes { func (o *imagePlugin) GetAttributes() volume.Attributes {
return volume.Attributes{ return volume.Attributes{
ReadOnly: true, ReadOnly: true,
Managed: true, Managed: false,
SELinuxRelabel: true, SELinuxRelabel: false,
} }
} }