diff --git a/pkg/volume/emptydir/empty_dir.go b/pkg/volume/emptydir/empty_dir.go index 161ed2bd8a2..332617ea55f 100644 --- a/pkg/volume/emptydir/empty_dir.go +++ b/pkg/volume/emptydir/empty_dir.go @@ -20,6 +20,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/kubelet/util/swap" @@ -50,6 +51,16 @@ import ( // https://issue.k8s.io/2630 const perm os.FileMode = 0777 +// unixModeToFileMode converts a Unix-style permission value (0-01777) to +// Go's os.FileMode. The sticky bit (01000) maps to os.ModeSticky. +func unixModeToFileMode(mode int32) os.FileMode { + fm := os.FileMode(mode & 0777) + if mode&01000 != 0 { + fm |= os.ModeSticky + } + return fm +} + // ProbeVolumePlugins is the primary entrypoint for volume plugins. func ProbeVolumePlugins() []volume.VolumePlugin { return []volume.VolumePlugin{ @@ -145,6 +156,7 @@ func calculateEmptyDirMemorySize(nodeAllocatableMemory *resource.Quantity, spec func (plugin *emptyDirPlugin) newMounterInternal(spec *volume.Spec, pod *v1.Pod, mounter mount.Interface, mountDetector mountDetector) (volume.Mounter, error) { medium := v1.StorageMediumDefault sizeLimit := &resource.Quantity{} + var mode *int32 if spec.Volume.EmptyDir != nil { // Support a non-specified source as EmptyDir. medium = spec.Volume.EmptyDir.Medium if medium == v1.StorageMediumMemory { @@ -154,12 +166,16 @@ func (plugin *emptyDirPlugin) newMounterInternal(spec *volume.Spec, pod *v1.Pod, } sizeLimit = calculateEmptyDirMemorySize(nodeAllocatable.Memory(), spec, pod) } + if utilfeature.DefaultFeatureGate.Enabled(features.EmptyDirVolumeMode) { + mode = spec.Volume.EmptyDir.Mode + } } return &emptyDir{ pod: pod, volName: spec.Name(), medium: medium, sizeLimit: sizeLimit, + mode: mode, mounter: mounter, mountDetector: mountDetector, plugin: plugin, @@ -214,6 +230,7 @@ type emptyDir struct { volName string sizeLimit *resource.Quantity medium v1.StorageMedium + mode *int32 mounter mount.Interface mountDetector mountDetector plugin *emptyDirPlugin @@ -447,13 +464,21 @@ func getPageSizeMountOption(medium v1.StorageMedium, pod *v1.Pod) (string, error return fmt.Sprintf("%s=%s", hugePagesPageSizeMountOption, pageSize.String()), nil } -// setupDir creates the directory with the default permissions specified by the perm constant. +// setupDir creates the directory with the requested mode, or defaults to 0777. func (ed *emptyDir) setupDir(dir string) error { // Create the directory if it doesn't already exist. if err := os.MkdirAll(dir, perm); err != nil { return err } + if ed.mode != nil && runtime.GOOS != "windows" { + effectivePerm := unixModeToFileMode(*ed.mode) + if err := os.Chmod(dir, effectivePerm); err != nil { + return err + } + return nil + } + // stat the directory to read permission bits fileinfo, err := os.Lstat(dir) if err != nil { diff --git a/pkg/volume/emptydir/empty_dir_test.go b/pkg/volume/emptydir/empty_dir_test.go index 44d3ef8bed2..4ee095f197c 100644 --- a/pkg/volume/emptydir/empty_dir_test.go +++ b/pkg/volume/emptydir/empty_dir_test.go @@ -43,6 +43,7 @@ import ( volumetest "k8s.io/kubernetes/pkg/volume/testing" volumeutil "k8s.io/kubernetes/pkg/volume/util" "k8s.io/mount-utils" + "k8s.io/utils/ptr" ) // Construct an instance of a plugin, by name. @@ -1294,6 +1295,7 @@ func TestResizeEphemeralVolume(t *testing.T) { }, }, } + pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ UID: types.UID("poduid"), @@ -1346,3 +1348,136 @@ func TestResizeEphemeralVolume(t *testing.T) { }) } } + +func TestEmptyDirVolumeMode(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EmptyDirVolumeMode, true) + + testCases := []struct { + name string + mode *int32 + expectedPerm os.FileMode + expectSticky bool + }{ + { + name: "mode 0750", + mode: ptr.To[int32](0o750), + expectedPerm: os.FileMode(0o750), + }, + { + name: "mode 01777 with sticky bit", + mode: ptr.To[int32](0o1777), + expectedPerm: os.FileMode(0o777), + expectSticky: true, + }, + { + name: "nil mode defaults to 0777", + mode: nil, + expectedPerm: os.FileMode(0o777), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + basePath, err := utiltesting.MkTmpdir("emptydir_mode_test") + if err != nil { + t.Fatalf("can't make a temp dir: %v", err) + } + t.Cleanup(func() { _ = os.RemoveAll(basePath) }) + + plug := makePluginUnderTest(t, "kubernetes.io/empty-dir", basePath) + + spec := &v1.Volume{ + Name: "test-volume", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{Mode: tc.mode}, + }, + } + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID("poduid"), + }, + } + + mounter, err := plug.(*emptyDirPlugin).newMounterInternal( + volume.NewSpecFromVolume(spec), + pod, + mount.NewFakeMounter(nil), + &fakeMountDetector{}, + ) + if err != nil { + t.Fatalf("Failed to make a new Mounter: %v", err) + } + + if err := mounter.SetUp(volume.MounterArgs{}); err != nil { + t.Fatalf("SetUp failed: %v", err) + } + + volPath := mounter.GetPath() + fileinfo, err := os.Stat(volPath) + if err != nil { + t.Fatalf("Stat failed: %v", err) + } + + if fileinfo.Mode().Perm() != tc.expectedPerm { + t.Errorf("expected permissions %v, got %v", tc.expectedPerm, fileinfo.Mode().Perm()) + } + + if tc.expectSticky { + if fileinfo.Mode()&os.ModeSticky == 0 { + t.Errorf("expected sticky bit to be set, got mode %v", fileinfo.Mode()) + } + } + }) + } +} + +func TestEmptyDirVolumeModeFeatureGateDisabled(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EmptyDirVolumeMode, false) + + basePath, err := utiltesting.MkTmpdir("emptydir_mode_gate_test") + if err != nil { + t.Fatalf("can't make a temp dir: %v", err) + } + t.Cleanup(func() { _ = os.RemoveAll(basePath) }) + + plug := makePluginUnderTest(t, "kubernetes.io/empty-dir", basePath) + + mode := int32(0o750) + spec := &v1.Volume{ + Name: "test-volume", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{Mode: &mode}, + }, + } + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID("poduid"), + }, + } + + mounter, err := plug.(*emptyDirPlugin).newMounterInternal( + volume.NewSpecFromVolume(spec), + pod, + mount.NewFakeMounter(nil), + &fakeMountDetector{}, + ) + if err != nil { + t.Fatalf("Failed to make a new Mounter: %v", err) + } + + if err := mounter.SetUp(volume.MounterArgs{}); err != nil { + t.Fatalf("SetUp failed: %v", err) + } + + volPath := mounter.GetPath() + fileinfo, err := os.Stat(volPath) + if err != nil { + t.Fatalf("Stat failed: %v", err) + } + + if fileinfo.Mode().Perm() != perm.Perm() { + t.Errorf("expected default permissions %v when feature gate is disabled, got %v", perm.Perm(), fileinfo.Mode().Perm()) + } +} diff --git a/pkg/volume/emptydir/empty_dir_windows_test.go b/pkg/volume/emptydir/empty_dir_windows_test.go new file mode 100644 index 00000000000..753c3ac6a3a --- /dev/null +++ b/pkg/volume/emptydir/empty_dir_windows_test.go @@ -0,0 +1,95 @@ +//go:build windows + +/* +Copyright 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 emptydir + +import ( + "os" + "testing" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + utilfeature "k8s.io/apiserver/pkg/util/feature" + utiltesting "k8s.io/client-go/util/testing" + featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/kubernetes/pkg/features" + "k8s.io/kubernetes/pkg/volume" + volumetest "k8s.io/kubernetes/pkg/volume/testing" + "k8s.io/mount-utils" +) + +type fakeMountDetector struct { + medium v1.StorageMedium + isMount bool +} + +func (fake *fakeMountDetector) GetMountMedium(path string, requestedMedium v1.StorageMedium) (v1.StorageMedium, bool, *resource.Quantity, error) { + return fake.medium, fake.isMount, nil, nil +} + +func TestEmptyDirVolumeModeWindows(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EmptyDirVolumeMode, true) + + basePath, err := utiltesting.MkTmpdir("emptydir_mode_windows_test") + if err != nil { + t.Fatalf("can't make a temp dir: %v", err) + } + defer os.RemoveAll(basePath) + + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), nil, volumetest.NewFakeVolumeHost(t, basePath, nil, nil)) + plug, err := plugMgr.FindPluginByName("kubernetes.io/empty-dir") + if err != nil { + t.Fatalf("Can't find the plugin by name: %v", err) + } + + mode := int32(0o750) + spec := &v1.Volume{ + Name: "test-volume", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{Mode: &mode}, + }, + } + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID("poduid"), + }, + } + + mounter, err := plug.(*emptyDirPlugin).newMounterInternal( + volume.NewSpecFromVolume(spec), + pod, + mount.NewFakeMounter(nil), + &fakeMountDetector{}, + ) + if err != nil { + t.Fatalf("Failed to make a new Mounter: %v", err) + } + + if err := mounter.SetUp(volume.MounterArgs{}); err != nil { + t.Fatalf("SetUp should not fail on Windows with mode set, got: %v", err) + } + + volPath := mounter.GetPath() + if _, err := os.Stat(volPath); err != nil { + t.Fatalf("directory should exist after SetUp: %v", err) + } +}