diff --git a/contrib/for-tests/mount-tester-user/Dockerfile b/contrib/for-tests/mount-tester-user/Dockerfile new file mode 100644 index 00000000000..70be763cc90 --- /dev/null +++ b/contrib/for-tests/mount-tester-user/Dockerfile @@ -0,0 +1,16 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# 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. + +FROM gcr.io/google-containers/mounttest:0.3 +USER 1001 diff --git a/contrib/for-tests/mount-tester-user/Makefile b/contrib/for-tests/mount-tester-user/Makefile new file mode 100644 index 00000000000..0cb05d763b5 --- /dev/null +++ b/contrib/for-tests/mount-tester-user/Makefile @@ -0,0 +1,9 @@ +all: push + +TAG = 0.1 + +image: + sudo docker build -t gcr.io/google_containers/mounttest-user:$(TAG) . + +push: image + gcloud docker push gcr.io/google_containers/mounttest-user:$(TAG) diff --git a/contrib/for-tests/mount-tester/Makefile b/contrib/for-tests/mount-tester/Makefile index 37d37859068..01e6f584a52 100644 --- a/contrib/for-tests/mount-tester/Makefile +++ b/contrib/for-tests/mount-tester/Makefile @@ -1,6 +1,6 @@ all: push -TAG = 0.2 +TAG = 0.3 mt: mt.go CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w' ./mt.go diff --git a/contrib/for-tests/mount-tester/mt.go b/contrib/for-tests/mount-tester/mt.go index 0a39d7cc86d..3e6fd3b382b 100644 --- a/contrib/for-tests/mount-tester/mt.go +++ b/contrib/for-tests/mount-tester/mt.go @@ -25,17 +25,23 @@ import ( ) var ( - fsTypePath = "" - fileModePath = "" - readFileContentPath = "" - readWriteNewFilePath = "" + fsTypePath = "" + fileModePath = "" + filePermPath = "" + readFileContentPath = "" + newFilePath0644 = "" + newFilePath0666 = "" + newFilePath0777 = "" ) func init() { flag.StringVar(&fsTypePath, "fs_type", "", "Path to print the fs type for") - flag.StringVar(&fileModePath, "file_mode", "", "Path to print the filemode of") + flag.StringVar(&fileModePath, "file_mode", "", "Path to print the mode bits of") + flag.StringVar(&filePermPath, "file_perm", "", "Path to print the perms of") flag.StringVar(&readFileContentPath, "file_content", "", "Path to read the file content from") - flag.StringVar(&readWriteNewFilePath, "rw_new_file", "", "Path to write to and read from") + flag.StringVar(&newFilePath0644, "new_file_0644", "", "Path to write to and read from with perm 0644") + flag.StringVar(&newFilePath0666, "new_file_0666", "", "Path to write to and read from with perm 0666") + flag.StringVar(&newFilePath0777, "new_file_0777", "", "Path to write to and read from with perm 0777") } // This program performs some tests on the filesystem as dictated by the @@ -48,6 +54,9 @@ func main() { errs = []error{} ) + // Clear the umask so we can set any mode bits we want. + syscall.Umask(0000) + // NOTE: the ordering of execution of the various command line // flags is intentional and allows a single command to: // @@ -62,7 +71,17 @@ func main() { errs = append(errs, err) } - err = readWriteNewFile(readWriteNewFilePath) + err = readWriteNewFile(newFilePath0644, 0644) + if err != nil { + errs = append(errs, err) + } + + err = readWriteNewFile(newFilePath0666, 0666) + if err != nil { + errs = append(errs, err) + } + + err = readWriteNewFile(newFilePath0777, 0777) if err != nil { errs = append(errs, err) } @@ -72,6 +91,11 @@ func main() { errs = append(errs, err) } + err = filePerm(filePermPath) + if err != nil { + errs = append(errs, err) + } + err = readFileContent(readFileContentPath) if err != nil { errs = append(errs, err) @@ -94,7 +118,7 @@ func fsType(path string) error { buf := syscall.Statfs_t{} if err := syscall.Statfs(path, &buf); err != nil { - fmt.Printf("error from statfs(%q): %v", path, err) + fmt.Printf("error from statfs(%q): %v\n", path, err) return err } @@ -122,6 +146,21 @@ func fileMode(path string) error { return nil } +func filePerm(path string) error { + if path == "" { + return nil + } + + fileinfo, err := os.Lstat(path) + if err != nil { + fmt.Printf("error from Lstat(%q): %v\n", path, err) + return err + } + + fmt.Printf("perms of file %q: %v\n", path, fileinfo.Mode().Perm()) + return nil +} + func readFileContent(path string) error { if path == "" { return nil @@ -138,13 +177,13 @@ func readFileContent(path string) error { return nil } -func readWriteNewFile(path string) error { +func readWriteNewFile(path string, perm os.FileMode) error { if path == "" { return nil } content := "mount-tester new file\n" - err := ioutil.WriteFile(path, []byte(content), 0644) + err := ioutil.WriteFile(path, []byte(content), perm) if err != nil { fmt.Printf("error writing new file %q: %v\n", path, err) return err diff --git a/pkg/securitycontext/util.go b/pkg/securitycontext/util.go index 64bf7e53ecd..fcdf0ae467d 100644 --- a/pkg/securitycontext/util.go +++ b/pkg/securitycontext/util.go @@ -16,7 +16,12 @@ limitations under the License. package securitycontext -import "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +import ( + "fmt" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) // HasPrivilegedRequest returns the value of SecurityContext.Privileged, taking into account // the possibility of nils @@ -41,3 +46,23 @@ func HasCapabilitiesRequest(container *api.Container) bool { } return len(container.SecurityContext.Capabilities.Add) > 0 || len(container.SecurityContext.Capabilities.Drop) > 0 } + +const expectedSELinuxContextFields = 4 + +// ParseSELinuxOptions parses a string containing a full SELinux context +// (user, role, type, and level) into an SELinuxOptions object. If the +// context is malformed, an error is returned. +func ParseSELinuxOptions(context string) (*api.SELinuxOptions, error) { + fields := strings.SplitN(context, ":", expectedSELinuxContextFields) + + if len(fields) != expectedSELinuxContextFields { + return nil, fmt.Errorf("expected %v fields in selinuxcontext; got %v (context: %v)", expectedSELinuxContextFields, len(fields), context) + } + + return &api.SELinuxOptions{ + User: fields[0], + Role: fields[1], + Type: fields[2], + Level: fields[3], + }, nil +} diff --git a/pkg/securitycontext/util_test.go b/pkg/securitycontext/util_test.go new file mode 100644 index 00000000000..978e1a6850f --- /dev/null +++ b/pkg/securitycontext/util_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 securitycontext + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +func TestParseSELinuxOptions(t *testing.T) { + cases := []struct { + name string + input string + expected *api.SELinuxOptions + }{ + { + name: "simple", + input: "user_t:role_t:type_t:s0", + expected: &api.SELinuxOptions{ + User: "user_t", + Role: "role_t", + Type: "type_t", + Level: "s0", + }, + }, + { + name: "simple + categories", + input: "user_t:role_t:type_t:s0:c0", + expected: &api.SELinuxOptions{ + User: "user_t", + Role: "role_t", + Type: "type_t", + Level: "s0:c0", + }, + }, + { + name: "not enough fields", + input: "type_t:s0:c0", + }, + } + + for _, tc := range cases { + result, err := ParseSELinuxOptions(tc.input) + + if err != nil { + if tc.expected == nil { + continue + } else { + t.Errorf("%v: unexpected error: %v", tc.name, err) + } + } + + compareContexts(tc.name, tc.expected, result, t) + } +} + +func compareContexts(name string, ex, ac *api.SELinuxOptions, t *testing.T) { + if e, a := ex.User, ac.User; e != a { + t.Errorf("%v: expected user: %v, got: %v", name, e, a) + } + if e, a := ex.Role, ac.Role; e != a { + t.Errorf("%v: expected role: %v, got: %v", name, e, a) + } + if e, a := ex.Type, ac.Type; e != a { + t.Errorf("%v: expected type: %v, got: %v", name, e, a) + } + if e, a := ex.Level, ac.Level; e != a { + t.Errorf("%v: expected level: %v, got: %v", name, e, a) + } +} diff --git a/pkg/volume/empty_dir/chcon_runner.go b/pkg/volume/empty_dir/chcon_runner.go new file mode 100644 index 00000000000..e18aefce6b1 --- /dev/null +++ b/pkg/volume/empty_dir/chcon_runner.go @@ -0,0 +1,27 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 empty_dir + +// chconRunner knows how to chcon a directory. +type chconRunner interface { + SetContext(dir, context string) error +} + +// newChconRunner returns a new chconRunner. +func newChconRunner() chconRunner { + return &realChconRunner{} +} diff --git a/pkg/volume/empty_dir/chcon_runner_linux.go b/pkg/volume/empty_dir/chcon_runner_linux.go new file mode 100644 index 00000000000..42abba25015 --- /dev/null +++ b/pkg/volume/empty_dir/chcon_runner_linux.go @@ -0,0 +1,34 @@ +// +build linux + +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 empty_dir + +import ( + "github.com/docker/libcontainer/selinux" +) + +type realChconRunner struct{} + +func (_ *realChconRunner) SetContext(dir, context string) error { + // If SELinux is not enabled, return an empty string + if !selinux.SelinuxEnabled() { + return nil + } + + return selinux.Setfilecon(dir, context) +} diff --git a/pkg/volume/empty_dir/chcon_runner_unsupported.go b/pkg/volume/empty_dir/chcon_runner_unsupported.go new file mode 100644 index 00000000000..4b75ef9d305 --- /dev/null +++ b/pkg/volume/empty_dir/chcon_runner_unsupported.go @@ -0,0 +1,26 @@ +// +build !linux + +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 empty_dir + +type realChconRunner struct{} + +func (_ *realChconRunner) SetContext(dir, context string) error { + // NOP + return nil +} diff --git a/pkg/volume/empty_dir/empty_dir.go b/pkg/volume/empty_dir/empty_dir.go index 6a189a62841..0c4cca2f48d 100644 --- a/pkg/volume/empty_dir/empty_dir.go +++ b/pkg/volume/empty_dir/empty_dir.go @@ -19,15 +19,24 @@ package empty_dir import ( "fmt" "os" + "path" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" + volumeutil "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/util" "github.com/golang/glog" ) +// TODO: in the near future, this will be changed to be more restrictive +// and the group will be set to allow containers to use emptyDir volumes +// from the group attribute. +// +// https://github.com/GoogleCloudPlatform/kubernetes/issues/2630 +const perm os.FileMode = 0777 + // This is the primary entrypoint for volume plugins. func ProbeVolumePlugins() []volume.VolumePlugin { return []volume.VolumePlugin{ @@ -61,22 +70,23 @@ func (plugin *emptyDirPlugin) CanSupport(spec *volume.Spec) bool { } func (plugin *emptyDirPlugin) NewBuilder(spec *volume.Spec, pod *api.Pod, opts volume.VolumeOptions, mounter mount.Interface) (volume.Builder, error) { - return plugin.newBuilderInternal(spec, pod, mounter, &realMountDetector{mounter}, opts) + return plugin.newBuilderInternal(spec, pod, mounter, &realMountDetector{mounter}, opts, newChconRunner()) } -func (plugin *emptyDirPlugin) newBuilderInternal(spec *volume.Spec, pod *api.Pod, mounter mount.Interface, mountDetector mountDetector, opts volume.VolumeOptions) (volume.Builder, error) { +func (plugin *emptyDirPlugin) newBuilderInternal(spec *volume.Spec, pod *api.Pod, mounter mount.Interface, mountDetector mountDetector, opts volume.VolumeOptions, chconRunner chconRunner) (volume.Builder, error) { medium := api.StorageMediumDefault if spec.VolumeSource.EmptyDir != nil { // Support a non-specified source as EmptyDir. medium = spec.VolumeSource.EmptyDir.Medium } return &emptyDir{ - podUID: pod.UID, + pod: pod, volName: spec.Name, medium: medium, mounter: mounter, mountDetector: mountDetector, plugin: plugin, rootContext: opts.RootContext, + chconRunner: chconRunner, }, nil } @@ -87,7 +97,7 @@ func (plugin *emptyDirPlugin) NewCleaner(volName string, podUID types.UID, mount func (plugin *emptyDirPlugin) newCleanerInternal(volName string, podUID types.UID, mounter mount.Interface, mountDetector mountDetector) (volume.Cleaner, error) { ed := &emptyDir{ - podUID: podUID, + pod: &api.Pod{ObjectMeta: api.ObjectMeta{UID: podUID}}, volName: volName, medium: api.StorageMediumDefault, // might be changed later mounter: mounter, @@ -117,13 +127,14 @@ const ( // EmptyDir volumes are temporary directories exposed to the pod. // These do not persist beyond the lifetime of a pod. type emptyDir struct { - podUID types.UID + pod *api.Pod volName string medium api.StorageMedium mounter mount.Interface mountDetector mountDetector plugin *emptyDirPlugin rootContext string + chconRunner chconRunner } // SetUp creates new directory. @@ -133,29 +144,58 @@ func (ed *emptyDir) SetUp() error { // SetUpAt creates new directory. func (ed *emptyDir) SetUpAt(dir string) error { + isMnt, err := ed.mounter.IsMountPoint(dir) + // Getting an os.IsNotExist err from is a contingency; the directory + // may not exist yet, in which case, setup should run. + if err != nil && !os.IsNotExist(err) { + return err + } + + // If the plugin readiness file is present for this volume, and the + // storage medium is the default, then the volume is ready. If the + // medium is memory, and a mountpoint is present, then the volume is + // ready. + if volumeutil.IsReady(ed.getMetaDir()) { + if ed.medium == api.StorageMediumMemory && isMnt { + return nil + } else if ed.medium == api.StorageMediumDefault { + return nil + } + } + + // Determine the effective SELinuxOptions to use for this volume. + securityContext := "" + if selinuxEnabled() { + securityContext = ed.rootContext + } + switch ed.medium { case api.StorageMediumDefault: - return ed.setupDefault(dir) + err = ed.setupDir(dir, securityContext) case api.StorageMediumMemory: - return ed.setupTmpfs(dir) + err = ed.setupTmpfs(dir, securityContext) default: - return fmt.Errorf("unknown storage medium %q", ed.medium) + err = fmt.Errorf("unknown storage medium %q", ed.medium) } + + if err == nil { + volumeutil.SetReady(ed.getMetaDir()) + } + + return err } func (ed *emptyDir) IsReadOnly() bool { return false } -func (ed *emptyDir) setupDefault(dir string) error { - return os.MkdirAll(dir, 0750) -} - -func (ed *emptyDir) setupTmpfs(dir string) error { +// setupTmpfs creates a tmpfs mount at the specified directory with the +// specified SELinux context. +func (ed *emptyDir) setupTmpfs(dir string, selinuxContext string) error { if ed.mounter == nil { return fmt.Errorf("memory storage requested, but mounter is nil") } - if err := os.MkdirAll(dir, 0750); err != nil { + if err := ed.setupDir(dir, selinuxContext); err != nil { return err } // Make SetUp idempotent. @@ -170,28 +210,66 @@ func (ed *emptyDir) setupTmpfs(dir string) error { } // By default a tmpfs mount will receive a different SELinux context - // from that of the Kubelet root directory which is not readable from - // the SELinux context of a docker container. - // - // getTmpfsMountOptions gets the mount option to set the context of - // the tmpfs mount so that it can be read from the SELinux context of - // the container. - opts := ed.getTmpfsMountOptions() - glog.V(3).Infof("pod %v: mounting tmpfs for volume %v with opts %v", ed.podUID, ed.volName, opts) + // which is not readable from the SELinux context of a docker container. + var opts []string + if selinuxContext != "" { + opts = []string{fmt.Sprintf("rootcontext=\"%v\"", selinuxContext)} + } else { + opts = []string{} + } + + glog.V(3).Infof("pod %v: mounting tmpfs for volume %v with opts %v", ed.pod.UID, ed.volName, opts) return ed.mounter.Mount("tmpfs", dir, "tmpfs", opts) } -func (ed *emptyDir) getTmpfsMountOptions() []string { - if ed.rootContext == "" { - return []string{""} +// setupDir creates the directory with the specified SELinux context and +// the default permissions specified by the perm constant. +func (ed *emptyDir) setupDir(dir, selinuxContext string) error { + // Create the directory if it doesn't already exist. + if err := os.MkdirAll(dir, perm); err != nil { + return err } - return []string{fmt.Sprintf("rootcontext=\"%v\"", ed.rootContext)} + // stat the directory to read permission bits + fileinfo, err := os.Lstat(dir) + if err != nil { + return err + } + + if fileinfo.Mode().Perm() != perm.Perm() { + // If the permissions on the created directory are wrong, the + // kubelet is probably running with a umask set. In order to + // avoid clearing the umask for the entire process or locking + // the thread, clearing the umask, creating the dir, restoring + // the umask, and unlocking the thread, we do a chmod to set + // the specific bits we need. + err := os.Chmod(dir, perm) + if err != nil { + return err + } + + fileinfo, err = os.Lstat(dir) + if err != nil { + return err + } + + if fileinfo.Mode().Perm() != perm.Perm() { + glog.Errorf("Expected directory %q permissions to be: %s; got: %s", dir, perm.Perm(), fileinfo.Mode().Perm()) + } + } + + // Set the context on the directory, if appropriate + if selinuxContext != "" { + glog.V(3).Infof("Setting SELinux context for %v to %v", dir, selinuxContext) + return ed.chconRunner.SetContext(dir, selinuxContext) + } + + return nil } func (ed *emptyDir) GetPath() string { name := emptyDirPluginName - return ed.plugin.host.GetPodVolumeDir(ed.podUID, util.EscapeQualifiedNameForDisk(name), ed.volName) + return ed.plugin.host.GetPodVolumeDir(ed.pod.UID, util.EscapeQualifiedNameForDisk(name), ed.volName) } // TearDown simply discards everything in the directory. @@ -238,3 +316,7 @@ func (ed *emptyDir) teardownTmpfs(dir string) error { } return nil } + +func (ed *emptyDir) getMetaDir() string { + return path.Join(ed.plugin.host.GetPodPluginDir(ed.pod.UID, util.EscapeQualifiedNameForDisk(emptyDirPluginName)), ed.volName) +} diff --git a/pkg/volume/empty_dir/empty_dir_linux.go b/pkg/volume/empty_dir/empty_dir_linux.go index aaf2a71fc54..57b5e5fd211 100644 --- a/pkg/volume/empty_dir/empty_dir_linux.go +++ b/pkg/volume/empty_dir/empty_dir_linux.go @@ -23,6 +23,7 @@ import ( "syscall" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount" + "github.com/docker/libcontainer/selinux" "github.com/golang/glog" ) @@ -51,3 +52,8 @@ func (m *realMountDetector) GetMountMedium(path string) (storageMedium, bool, er } return mediumUnknown, isMnt, nil } + +// selinuxEnabled determines whether SELinux is enabled. +func selinuxEnabled() bool { + return selinux.SelinuxEnabled() +} diff --git a/pkg/volume/empty_dir/empty_dir_test.go b/pkg/volume/empty_dir/empty_dir_test.go index e61e61baffa..9c3aba54351 100644 --- a/pkg/volume/empty_dir/empty_dir_test.go +++ b/pkg/volume/empty_dir/empty_dir_test.go @@ -17,6 +17,7 @@ limitations under the License. package empty_dir import ( + "io/ioutil" "os" "path" "testing" @@ -25,13 +26,11 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/util" ) -// The dir where volumes will be stored. -const basePath = "/tmp/fake" - // Construct an instance of a plugin, by name. -func makePluginUnderTest(t *testing.T, plugName string) volume.VolumePlugin { +func makePluginUnderTest(t *testing.T, plugName, basePath string) volume.VolumePlugin { plugMgr := volume.VolumePluginMgr{} plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost(basePath, nil, nil)) @@ -43,7 +42,7 @@ func makePluginUnderTest(t *testing.T, plugName string) volume.VolumePlugin { } func TestCanSupport(t *testing.T) { - plug := makePluginUnderTest(t, "kubernetes.io/empty-dir") + plug := makePluginUnderTest(t, "kubernetes.io/empty-dir", "/tmp/fake") if plug.Name() != "kubernetes.io/empty-dir" { t.Errorf("Wrong name: %s", plug.Name()) @@ -65,77 +64,132 @@ func (fake *fakeMountDetector) GetMountMedium(path string) (storageMedium, bool, return fake.medium, fake.isMount, nil } -func TestPlugin(t *testing.T) { - plug := makePluginUnderTest(t, "kubernetes.io/empty-dir") +type fakeChconRequest struct { + dir string + context string +} - spec := &api.Volume{ - Name: "vol1", - VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{Medium: api.StorageMediumDefault}}, - } - mounter := mount.FakeMounter{} - mountDetector := fakeMountDetector{} - pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("poduid")}} - builder, err := plug.(*emptyDirPlugin).newBuilderInternal(volume.NewSpecFromVolume(spec), pod, &mounter, &mountDetector, volume.VolumeOptions{""}) - if err != nil { - t.Errorf("Failed to make a new Builder: %v", err) - } - if builder == nil { - t.Errorf("Got a nil Builder") +type fakeChconRunner struct { + requests []fakeChconRequest +} + +func newFakeChconRunner() *fakeChconRunner { + return &fakeChconRunner{} +} + +func (f *fakeChconRunner) SetContext(dir, context string) error { + f.requests = append(f.requests, fakeChconRequest{dir, context}) + + return nil +} + +func TestPluginEmptyRootContext(t *testing.T) { + doTestPlugin(t, pluginTestConfig{ + medium: api.StorageMediumDefault, + rootContext: "", + expectedChcons: 0, + expectedSetupMounts: 0, + expectedTeardownMounts: 0}) +} + +func TestPluginRootContextSet(t *testing.T) { + if !selinuxEnabled() { + return } - volPath := builder.GetPath() - if volPath != path.Join(basePath, "pods/poduid/volumes/kubernetes.io~empty-dir/vol1") { - t.Errorf("Got unexpected path: %s", volPath) - } - - if err := builder.SetUp(); err != nil { - t.Errorf("Expected success, got: %v", err) - } - if _, err := os.Stat(volPath); err != nil { - if os.IsNotExist(err) { - t.Errorf("SetUp() failed, volume path not created: %s", volPath) - } else { - t.Errorf("SetUp() failed: %v", err) - } - } - if len(mounter.Log) != 0 { - t.Errorf("Expected 0 mounter calls, got %#v", mounter.Log) - } - mounter.ResetLog() - - cleaner, err := plug.(*emptyDirPlugin).newCleanerInternal("vol1", types.UID("poduid"), &mounter, &fakeMountDetector{}) - if err != nil { - t.Errorf("Failed to make a new Cleaner: %v", err) - } - if cleaner == nil { - t.Errorf("Got a nil Cleaner") - } - - if err := cleaner.TearDown(); err != nil { - t.Errorf("Expected success, got: %v", err) - } - if _, err := os.Stat(volPath); err == nil { - t.Errorf("TearDown() failed, volume path still exists: %s", volPath) - } else if !os.IsNotExist(err) { - t.Errorf("SetUp() failed: %v", err) - } - if len(mounter.Log) != 0 { - t.Errorf("Expected 0 mounter calls, got %#v", mounter.Log) - } - mounter.ResetLog() + doTestPlugin(t, pluginTestConfig{ + medium: api.StorageMediumDefault, + rootContext: "user:role:type:range", + expectedSELinuxContext: "user:role:type:range", + expectedChcons: 1, + expectedSetupMounts: 0, + expectedTeardownMounts: 0}) } func TestPluginTmpfs(t *testing.T) { - plug := makePluginUnderTest(t, "kubernetes.io/empty-dir") - - spec := &api.Volume{ - Name: "vol1", - VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{Medium: api.StorageMediumMemory}}, + if !selinuxEnabled() { + return } - mounter := mount.FakeMounter{} - mountDetector := fakeMountDetector{} - pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("poduid")}} - builder, err := plug.(*emptyDirPlugin).newBuilderInternal(volume.NewSpecFromVolume(spec), pod, &mounter, &mountDetector, volume.VolumeOptions{""}) + + doTestPlugin(t, pluginTestConfig{ + medium: api.StorageMediumMemory, + rootContext: "user:role:type:range", + expectedSELinuxContext: "user:role:type:range", + expectedChcons: 1, + expectedSetupMounts: 1, + shouldBeMountedBeforeTeardown: true, + expectedTeardownMounts: 1}) +} + +type pluginTestConfig struct { + medium api.StorageMedium + rootContext string + SELinuxOptions *api.SELinuxOptions + idempotent bool + expectedSELinuxContext string + expectedChcons int + expectedSetupMounts int + shouldBeMountedBeforeTeardown bool + expectedTeardownMounts int +} + +// doTestPlugin sets up a volume and tears it back down. +func doTestPlugin(t *testing.T, config pluginTestConfig) { + basePath, err := ioutil.TempDir("/tmp", "emptydir_volume_test") + if err != nil { + t.Fatalf("can't make a temp rootdir") + } + + var ( + volumePath = path.Join(basePath, "pods/poduid/volumes/kubernetes.io~empty-dir/test-volume") + metadataDir = path.Join(basePath, "pods/poduid/plugins/kubernetes.io~empty-dir/test-volume") + + plug = makePluginUnderTest(t, "kubernetes.io/empty-dir", basePath) + volumeName = "test-volume" + spec = &api.Volume{ + Name: volumeName, + VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{Medium: config.medium}}, + } + + mounter = mount.FakeMounter{} + mountDetector = fakeMountDetector{} + pod = &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("poduid")}} + fakeChconRnr = &fakeChconRunner{} + ) + + // Set up the SELinux options on the pod + if config.SELinuxOptions != nil { + pod.Spec = api.PodSpec{ + Containers: []api.Container{ + { + SecurityContext: &api.SecurityContext{ + SELinuxOptions: config.SELinuxOptions, + }, + VolumeMounts: []api.VolumeMount{ + { + Name: volumeName, + }, + }, + }, + }, + } + } + + if config.idempotent { + mounter.MountPoints = []mount.MountPoint{ + { + Path: volumePath, + }, + } + util.SetReady(metadataDir) + } + + builder, err := plug.(*emptyDirPlugin).newBuilderInternal(volume.NewSpecFromVolume(spec), + pod, + &mounter, + &mountDetector, + volume.VolumeOptions{config.rootContext}, + fakeChconRnr) if err != nil { t.Errorf("Failed to make a new Builder: %v", err) } @@ -144,30 +198,62 @@ func TestPluginTmpfs(t *testing.T) { } volPath := builder.GetPath() - if volPath != path.Join(basePath, "pods/poduid/volumes/kubernetes.io~empty-dir/vol1") { + if volPath != volumePath { t.Errorf("Got unexpected path: %s", volPath) } if err := builder.SetUp(); err != nil { t.Errorf("Expected success, got: %v", err) } - if _, err := os.Stat(volPath); err != nil { - if os.IsNotExist(err) { - t.Errorf("SetUp() failed, volume path not created: %s", volPath) - } else { - t.Errorf("SetUp() failed: %v", err) + + // Stat the directory and check the permission bits + fileinfo, err := os.Stat(volPath) + if !config.idempotent { + if err != nil { + if os.IsNotExist(err) { + t.Errorf("SetUp() failed, volume path not created: %s", volPath) + } else { + t.Errorf("SetUp() failed: %v", err) + } + } + if e, a := perm, fileinfo.Mode().Perm(); e != a { + t.Errorf("Unexpected file mode for %v: expected: %v, got: %v", volPath, e, a) + } + } else if err == nil { + // If this test is for idempotency and we were able + // to stat the volume path, it's an error. + t.Errorf("Volume directory was created unexpectedly") + } + + // Check the number of chcons during setup + if e, a := config.expectedChcons, len(fakeChconRnr.requests); e != a { + t.Errorf("Expected %v chcon calls, got %v", e, a) + } + if config.expectedChcons == 1 { + if e, a := config.expectedSELinuxContext, fakeChconRnr.requests[0].context; e != a { + t.Errorf("Unexpected chcon context argument; expected: %v, got: %v", e, a) + } + if e, a := volPath, fakeChconRnr.requests[0].dir; e != a { + t.Errorf("Unexpected chcon path argument: expected: %v, got: %v", e, a) } } - if len(mounter.Log) != 1 { - t.Errorf("Expected 1 mounter call, got %#v", mounter.Log) - } else { - if mounter.Log[0].Action != mount.FakeActionMount || mounter.Log[0].FSType != "tmpfs" { - t.Errorf("Unexpected mounter action: %#v", mounter.Log[0]) - } + + // Check the number of mounts performed during setup + if e, a := config.expectedSetupMounts, len(mounter.Log); e != a { + t.Errorf("Expected %v mounter calls during setup, got %v", e, a) + } else if config.expectedSetupMounts == 1 && + (mounter.Log[0].Action != mount.FakeActionMount || mounter.Log[0].FSType != "tmpfs") { + t.Errorf("Unexpected mounter action during setup: %#v", mounter.Log[0]) } mounter.ResetLog() - cleaner, err := plug.(*emptyDirPlugin).newCleanerInternal("vol1", types.UID("poduid"), &mounter, &fakeMountDetector{mediumMemory, true}) + // Make a cleaner for the volume + teardownMedium := mediumUnknown + if config.medium == api.StorageMediumMemory { + teardownMedium = mediumMemory + } + cleanerMountDetector := &fakeMountDetector{medium: teardownMedium, isMount: config.shouldBeMountedBeforeTeardown} + cleaner, err := plug.(*emptyDirPlugin).newCleanerInternal(volumeName, types.UID("poduid"), &mounter, cleanerMountDetector) if err != nil { t.Errorf("Failed to make a new Cleaner: %v", err) } @@ -175,6 +261,7 @@ func TestPluginTmpfs(t *testing.T) { t.Errorf("Got a nil Cleaner") } + // Tear down the volume if err := cleaner.TearDown(); err != nil { t.Errorf("Expected success, got: %v", err) } @@ -183,18 +270,19 @@ func TestPluginTmpfs(t *testing.T) { } else if !os.IsNotExist(err) { t.Errorf("SetUp() failed: %v", err) } - if len(mounter.Log) != 1 { - t.Errorf("Expected 1 mounter call, got %d (%v)", len(mounter.Log), mounter.Log) - } else { - if mounter.Log[0].Action != mount.FakeActionUnmount { - t.Errorf("Unexpected mounter action: %#v", mounter.Log[0]) - } + + // Check the number of mounter calls during tardown + if e, a := config.expectedTeardownMounts, len(mounter.Log); e != a { + t.Errorf("Expected %v mounter calls during teardown, got %v", e, a) + } else if config.expectedTeardownMounts == 1 && mounter.Log[0].Action != mount.FakeActionUnmount { + t.Errorf("Unexpected mounter action during teardown: %#v", mounter.Log[0]) } mounter.ResetLog() } func TestPluginBackCompat(t *testing.T) { - plug := makePluginUnderTest(t, "kubernetes.io/empty-dir") + basePath := "/tmp/fake" + plug := makePluginUnderTest(t, "kubernetes.io/empty-dir", basePath) spec := &api.Volume{ Name: "vol1", diff --git a/pkg/volume/empty_dir/empty_dir_unsupported.go b/pkg/volume/empty_dir/empty_dir_unsupported.go index 7589fe7d332..845fdea3c1a 100644 --- a/pkg/volume/empty_dir/empty_dir_unsupported.go +++ b/pkg/volume/empty_dir/empty_dir_unsupported.go @@ -18,7 +18,9 @@ limitations under the License. package empty_dir -import "github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount" +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount" +) // realMountDetector pretends to implement mediumer. type realMountDetector struct { @@ -28,3 +30,7 @@ type realMountDetector struct { func (m *realMountDetector) GetMountMedium(path string) (storageMedium, bool, error) { return mediumUnknown, false, nil } + +func selinuxEnabled() bool { + return false +} diff --git a/test/e2e/empty_dir.go b/test/e2e/empty_dir.go index 473789ad5da..2c3fdaf8f7a 100644 --- a/test/e2e/empty_dir.go +++ b/test/e2e/empty_dir.go @@ -27,51 +27,182 @@ import ( . "github.com/onsi/ginkgo" ) +const ( + testImageRootUid = "gcr.io/google_containers/mounttest:0.3" + testImageNonRootUid = "gcr.io/google_containers/mounttest-user:0.1" +) + var _ = Describe("EmptyDir volumes", func() { f := NewFramework("emptydir") - It("should have the correct mode", func() { - volumePath := "/test-volume" - source := &api.EmptyDirVolumeSource{ - Medium: api.StorageMediumMemory, - } - pod := testPodWithVolume(volumePath, source) - - pod.Spec.Containers[0].Args = []string{ - fmt.Sprintf("--fs_type=%v", volumePath), - fmt.Sprintf("--file_mode=%v", volumePath), - } - f.TestContainerOutput("emptydir r/w on tmpfs", pod, 0, []string{ - "mount type of \"/test-volume\": tmpfs", - "mode of file \"/test-volume\": dtrwxrwxrwx", // we expect the sticky bit (mode flag t) to be set for the dir - }) + It("volume on tmpfs should have the correct mode", func() { + doTestVolumeMode(f, testImageRootUid, api.StorageMediumMemory) }) - It("should support r/w", func() { - volumePath := "/test-volume" - filePath := path.Join(volumePath, "test-file") - source := &api.EmptyDirVolumeSource{ - Medium: api.StorageMediumMemory, - } - pod := testPodWithVolume(volumePath, source) + It("should support (root,0644,tmpfs)", func() { + doTest0644(f, testImageRootUid, api.StorageMediumMemory) + }) - pod.Spec.Containers[0].Args = []string{ - fmt.Sprintf("--fs_type=%v", volumePath), - fmt.Sprintf("--rw_new_file=%v", filePath), - fmt.Sprintf("--file_mode=%v", filePath), - } - f.TestContainerOutput("emptydir r/w on tmpfs", pod, 0, []string{ - "mount type of \"/test-volume\": tmpfs", - "mode of file \"/test-volume/test-file\": -rw-r--r--", - "content of file \"/test-volume/test-file\": mount-tester new file", - }) + It("should support (root,0666,tmpfs)", func() { + doTest0666(f, testImageRootUid, api.StorageMediumMemory) + }) + + It("should support (root,0777,tmpfs)", func() { + doTest0777(f, testImageRootUid, api.StorageMediumMemory) + }) + + It("should support (non-root,0644,tmpfs)", func() { + doTest0644(f, testImageNonRootUid, api.StorageMediumMemory) + }) + + It("should support (non-root,0666,tmpfs)", func() { + doTest0666(f, testImageNonRootUid, api.StorageMediumMemory) + }) + + It("should support (non-root,0777,tmpfs)", func() { + doTest0777(f, testImageNonRootUid, api.StorageMediumMemory) + }) + + It("volume on default medium should have the correct mode", func() { + doTestVolumeMode(f, testImageRootUid, api.StorageMediumDefault) + }) + + It("should support (root,0644,default)", func() { + doTest0644(f, testImageRootUid, api.StorageMediumDefault) + }) + + It("should support (root,0666,default)", func() { + doTest0666(f, testImageRootUid, api.StorageMediumDefault) + }) + + It("should support (root,0777,default)", func() { + doTest0777(f, testImageRootUid, api.StorageMediumDefault) + }) + + It("should support (non-root,0644,default)", func() { + doTest0644(f, testImageNonRootUid, api.StorageMediumDefault) + }) + + It("should support (non-root,0666,default)", func() { + doTest0666(f, testImageNonRootUid, api.StorageMediumDefault) + }) + + It("should support (non-root,0777,default)", func() { + doTest0777(f, testImageNonRootUid, api.StorageMediumDefault) }) }) -const containerName = "test-container" -const volumeName = "test-volume" +const ( + containerName = "test-container" + volumeName = "test-volume" +) -func testPodWithVolume(path string, source *api.EmptyDirVolumeSource) *api.Pod { +func doTestVolumeMode(f *Framework, image string, medium api.StorageMedium) { + var ( + volumePath = "/test-volume" + source = &api.EmptyDirVolumeSource{Medium: medium} + pod = testPodWithVolume(testImageRootUid, volumePath, source) + ) + + pod.Spec.Containers[0].Args = []string{ + fmt.Sprintf("--fs_type=%v", volumePath), + fmt.Sprintf("--file_perm=%v", volumePath), + } + + msg := fmt.Sprintf("emptydir volume type on %v", formatMedium(medium)) + out := []string{ + "perms of file \"/test-volume\": -rwxrwxrwx", + } + if medium == api.StorageMediumMemory { + out = append(out, "mount type of \"/test-volume\": tmpfs") + } + f.TestContainerOutput(msg, pod, 0, out) +} + +func doTest0644(f *Framework, image string, medium api.StorageMedium) { + var ( + volumePath = "/test-volume" + filePath = path.Join(volumePath, "test-file") + source = &api.EmptyDirVolumeSource{Medium: medium} + pod = testPodWithVolume(image, volumePath, source) + ) + + pod.Spec.Containers[0].Args = []string{ + fmt.Sprintf("--fs_type=%v", volumePath), + fmt.Sprintf("--new_file_0644=%v", filePath), + fmt.Sprintf("--file_perm=%v", filePath), + } + + msg := fmt.Sprintf("emptydir 0644 on %v", formatMedium(medium)) + out := []string{ + "perms of file \"/test-volume/test-file\": -rw-r--r--", + "content of file \"/test-volume/test-file\": mount-tester new file", + } + if medium == api.StorageMediumMemory { + out = append(out, "mount type of \"/test-volume\": tmpfs") + } + f.TestContainerOutput(msg, pod, 0, out) +} + +func doTest0666(f *Framework, image string, medium api.StorageMedium) { + var ( + volumePath = "/test-volume" + filePath = path.Join(volumePath, "test-file") + source = &api.EmptyDirVolumeSource{Medium: medium} + pod = testPodWithVolume(image, volumePath, source) + ) + + pod.Spec.Containers[0].Args = []string{ + fmt.Sprintf("--fs_type=%v", volumePath), + fmt.Sprintf("--new_file_0666=%v", filePath), + fmt.Sprintf("--file_perm=%v", filePath), + } + + msg := fmt.Sprintf("emptydir 0666 on %v", formatMedium(medium)) + out := []string{ + "perms of file \"/test-volume/test-file\": -rw-rw-rw-", + "content of file \"/test-volume/test-file\": mount-tester new file", + } + if medium == api.StorageMediumMemory { + out = append(out, "mount type of \"/test-volume\": tmpfs") + } + f.TestContainerOutput(msg, pod, 0, out) +} + +func doTest0777(f *Framework, image string, medium api.StorageMedium) { + var ( + volumePath = "/test-volume" + filePath = path.Join(volumePath, "test-file") + source = &api.EmptyDirVolumeSource{Medium: medium} + pod = testPodWithVolume(image, volumePath, source) + ) + + pod.Spec.Containers[0].Args = []string{ + fmt.Sprintf("--fs_type=%v", volumePath), + fmt.Sprintf("--new_file_0777=%v", filePath), + fmt.Sprintf("--file_perm=%v", filePath), + } + + msg := fmt.Sprintf("emptydir 0777 on %v", formatMedium(medium)) + out := []string{ + "perms of file \"/test-volume/test-file\": -rwxrwxrwx", + "content of file \"/test-volume/test-file\": mount-tester new file", + } + if medium == api.StorageMediumMemory { + out = append(out, "mount type of \"/test-volume\": tmpfs") + } + f.TestContainerOutput(msg, pod, 0, out) +} + +func formatMedium(medium api.StorageMedium) string { + if medium == api.StorageMediumMemory { + return "tmpfs" + } + + return "node default medium" +} + +func testPodWithVolume(image, path string, source *api.EmptyDirVolumeSource) *api.Pod { podName := "pod-" + string(util.NewUUID()) return &api.Pod{ @@ -86,7 +217,7 @@ func testPodWithVolume(path string, source *api.EmptyDirVolumeSource) *api.Pod { Containers: []api.Container{ { Name: containerName, - Image: "gcr.io/google_containers/mounttest:0.2", + Image: image, VolumeMounts: []api.VolumeMount{ { Name: volumeName,