Make emptyDir volumes work for non-root UIDs

pull/6/head
Paul Morie 2015-07-07 12:40:55 -04:00
parent 63cf00d24f
commit 5394aa979f
14 changed files with 739 additions and 165 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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{}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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",

View File

@ -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
}

View File

@ -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,