Make emptyDir volumes work for non-root UIDs

This commit is contained in:
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 all: push
TAG = 0.2 TAG = 0.3
mt: mt.go mt: mt.go
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w' ./mt.go CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w' ./mt.go

View File

@ -25,17 +25,23 @@ import (
) )
var ( var (
fsTypePath = "" fsTypePath = ""
fileModePath = "" fileModePath = ""
readFileContentPath = "" filePermPath = ""
readWriteNewFilePath = "" readFileContentPath = ""
newFilePath0644 = ""
newFilePath0666 = ""
newFilePath0777 = ""
) )
func init() { func init() {
flag.StringVar(&fsTypePath, "fs_type", "", "Path to print the fs type for") 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(&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 // This program performs some tests on the filesystem as dictated by the
@ -48,6 +54,9 @@ func main() {
errs = []error{} 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 // NOTE: the ordering of execution of the various command line
// flags is intentional and allows a single command to: // flags is intentional and allows a single command to:
// //
@ -62,7 +71,17 @@ func main() {
errs = append(errs, err) 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 { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
@ -72,6 +91,11 @@ func main() {
errs = append(errs, err) errs = append(errs, err)
} }
err = filePerm(filePermPath)
if err != nil {
errs = append(errs, err)
}
err = readFileContent(readFileContentPath) err = readFileContent(readFileContentPath)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
@ -94,7 +118,7 @@ func fsType(path string) error {
buf := syscall.Statfs_t{} buf := syscall.Statfs_t{}
if err := syscall.Statfs(path, &buf); err != nil { 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 return err
} }
@ -122,6 +146,21 @@ func fileMode(path string) error {
return nil 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 { func readFileContent(path string) error {
if path == "" { if path == "" {
return nil return nil
@ -138,13 +177,13 @@ func readFileContent(path string) error {
return nil return nil
} }
func readWriteNewFile(path string) error { func readWriteNewFile(path string, perm os.FileMode) error {
if path == "" { if path == "" {
return nil return nil
} }
content := "mount-tester new file\n" content := "mount-tester new file\n"
err := ioutil.WriteFile(path, []byte(content), 0644) err := ioutil.WriteFile(path, []byte(content), perm)
if err != nil { if err != nil {
fmt.Printf("error writing new file %q: %v\n", path, err) fmt.Printf("error writing new file %q: %v\n", path, err)
return err return err

View File

@ -16,7 +16,12 @@ limitations under the License.
package securitycontext 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 // HasPrivilegedRequest returns the value of SecurityContext.Privileged, taking into account
// the possibility of nils // 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 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 ( import (
"fmt" "fmt"
"os" "os"
"path"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/types"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount"
"github.com/GoogleCloudPlatform/kubernetes/pkg/volume" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume"
volumeutil "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/util"
"github.com/golang/glog" "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. // This is the primary entrypoint for volume plugins.
func ProbeVolumePlugins() []volume.VolumePlugin { func ProbeVolumePlugins() []volume.VolumePlugin {
return []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) { 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 medium := api.StorageMediumDefault
if spec.VolumeSource.EmptyDir != nil { // Support a non-specified source as EmptyDir. if spec.VolumeSource.EmptyDir != nil { // Support a non-specified source as EmptyDir.
medium = spec.VolumeSource.EmptyDir.Medium medium = spec.VolumeSource.EmptyDir.Medium
} }
return &emptyDir{ return &emptyDir{
podUID: pod.UID, pod: pod,
volName: spec.Name, volName: spec.Name,
medium: medium, medium: medium,
mounter: mounter, mounter: mounter,
mountDetector: mountDetector, mountDetector: mountDetector,
plugin: plugin, plugin: plugin,
rootContext: opts.RootContext, rootContext: opts.RootContext,
chconRunner: chconRunner,
}, nil }, 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) { func (plugin *emptyDirPlugin) newCleanerInternal(volName string, podUID types.UID, mounter mount.Interface, mountDetector mountDetector) (volume.Cleaner, error) {
ed := &emptyDir{ ed := &emptyDir{
podUID: podUID, pod: &api.Pod{ObjectMeta: api.ObjectMeta{UID: podUID}},
volName: volName, volName: volName,
medium: api.StorageMediumDefault, // might be changed later medium: api.StorageMediumDefault, // might be changed later
mounter: mounter, mounter: mounter,
@ -117,13 +127,14 @@ const (
// EmptyDir volumes are temporary directories exposed to the pod. // EmptyDir volumes are temporary directories exposed to the pod.
// These do not persist beyond the lifetime of a pod. // These do not persist beyond the lifetime of a pod.
type emptyDir struct { type emptyDir struct {
podUID types.UID pod *api.Pod
volName string volName string
medium api.StorageMedium medium api.StorageMedium
mounter mount.Interface mounter mount.Interface
mountDetector mountDetector mountDetector mountDetector
plugin *emptyDirPlugin plugin *emptyDirPlugin
rootContext string rootContext string
chconRunner chconRunner
} }
// SetUp creates new directory. // SetUp creates new directory.
@ -133,29 +144,58 @@ func (ed *emptyDir) SetUp() error {
// SetUpAt creates new directory. // SetUpAt creates new directory.
func (ed *emptyDir) SetUpAt(dir string) error { 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 { switch ed.medium {
case api.StorageMediumDefault: case api.StorageMediumDefault:
return ed.setupDefault(dir) err = ed.setupDir(dir, securityContext)
case api.StorageMediumMemory: case api.StorageMediumMemory:
return ed.setupTmpfs(dir) err = ed.setupTmpfs(dir, securityContext)
default: 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 { func (ed *emptyDir) IsReadOnly() bool {
return false return false
} }
func (ed *emptyDir) setupDefault(dir string) error { // setupTmpfs creates a tmpfs mount at the specified directory with the
return os.MkdirAll(dir, 0750) // specified SELinux context.
} func (ed *emptyDir) setupTmpfs(dir string, selinuxContext string) error {
func (ed *emptyDir) setupTmpfs(dir string) error {
if ed.mounter == nil { if ed.mounter == nil {
return fmt.Errorf("memory storage requested, but mounter is 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 return err
} }
// Make SetUp idempotent. // 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 // By default a tmpfs mount will receive a different SELinux context
// from that of the Kubelet root directory which is not readable from // which is not readable from the SELinux context of a docker container.
// the SELinux context of a docker container. var opts []string
// if selinuxContext != "" {
// getTmpfsMountOptions gets the mount option to set the context of opts = []string{fmt.Sprintf("rootcontext=\"%v\"", selinuxContext)}
// the tmpfs mount so that it can be read from the SELinux context of } else {
// the container. opts = []string{}
opts := ed.getTmpfsMountOptions() }
glog.V(3).Infof("pod %v: mounting tmpfs for volume %v with opts %v", ed.podUID, ed.volName, opts)
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) return ed.mounter.Mount("tmpfs", dir, "tmpfs", opts)
} }
func (ed *emptyDir) getTmpfsMountOptions() []string { // setupDir creates the directory with the specified SELinux context and
if ed.rootContext == "" { // the default permissions specified by the perm constant.
return []string{""} 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 { func (ed *emptyDir) GetPath() string {
name := emptyDirPluginName 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. // TearDown simply discards everything in the directory.
@ -238,3 +316,7 @@ func (ed *emptyDir) teardownTmpfs(dir string) error {
} }
return nil 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" "syscall"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount"
"github.com/docker/libcontainer/selinux"
"github.com/golang/glog" "github.com/golang/glog"
) )
@ -51,3 +52,8 @@ func (m *realMountDetector) GetMountMedium(path string) (storageMedium, bool, er
} }
return mediumUnknown, isMnt, nil 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 package empty_dir
import ( import (
"io/ioutil"
"os" "os"
"path" "path"
"testing" "testing"
@ -25,13 +26,11 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/types"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount"
"github.com/GoogleCloudPlatform/kubernetes/pkg/volume" "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. // 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 := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost(basePath, nil, nil)) 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) { 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" { if plug.Name() != "kubernetes.io/empty-dir" {
t.Errorf("Wrong name: %s", plug.Name()) 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 return fake.medium, fake.isMount, nil
} }
func TestPlugin(t *testing.T) { type fakeChconRequest struct {
plug := makePluginUnderTest(t, "kubernetes.io/empty-dir") dir string
context string
}
spec := &api.Volume{ type fakeChconRunner struct {
Name: "vol1", requests []fakeChconRequest
VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{Medium: api.StorageMediumDefault}}, }
}
mounter := mount.FakeMounter{} func newFakeChconRunner() *fakeChconRunner {
mountDetector := fakeMountDetector{} return &fakeChconRunner{}
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 { func (f *fakeChconRunner) SetContext(dir, context string) error {
t.Errorf("Failed to make a new Builder: %v", err) f.requests = append(f.requests, fakeChconRequest{dir, context})
}
if builder == nil { return nil
t.Errorf("Got a nil Builder") }
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() doTestPlugin(t, pluginTestConfig{
if volPath != path.Join(basePath, "pods/poduid/volumes/kubernetes.io~empty-dir/vol1") { medium: api.StorageMediumDefault,
t.Errorf("Got unexpected path: %s", volPath) rootContext: "user:role:type:range",
} expectedSELinuxContext: "user:role:type:range",
expectedChcons: 1,
if err := builder.SetUp(); err != nil { expectedSetupMounts: 0,
t.Errorf("Expected success, got: %v", err) expectedTeardownMounts: 0})
}
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()
} }
func TestPluginTmpfs(t *testing.T) { func TestPluginTmpfs(t *testing.T) {
plug := makePluginUnderTest(t, "kubernetes.io/empty-dir") if !selinuxEnabled() {
return
spec := &api.Volume{
Name: "vol1",
VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{Medium: api.StorageMediumMemory}},
} }
mounter := mount.FakeMounter{}
mountDetector := fakeMountDetector{} doTestPlugin(t, pluginTestConfig{
pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("poduid")}} medium: api.StorageMediumMemory,
builder, err := plug.(*emptyDirPlugin).newBuilderInternal(volume.NewSpecFromVolume(spec), pod, &mounter, &mountDetector, volume.VolumeOptions{""}) 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 { if err != nil {
t.Errorf("Failed to make a new Builder: %v", err) t.Errorf("Failed to make a new Builder: %v", err)
} }
@ -144,30 +198,62 @@ func TestPluginTmpfs(t *testing.T) {
} }
volPath := builder.GetPath() 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) t.Errorf("Got unexpected path: %s", volPath)
} }
if err := builder.SetUp(); err != nil { if err := builder.SetUp(); err != nil {
t.Errorf("Expected success, got: %v", err) t.Errorf("Expected success, got: %v", err)
} }
if _, err := os.Stat(volPath); err != nil {
if os.IsNotExist(err) { // Stat the directory and check the permission bits
t.Errorf("SetUp() failed, volume path not created: %s", volPath) fileinfo, err := os.Stat(volPath)
} else { if !config.idempotent {
t.Errorf("SetUp() failed: %v", err) 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) // Check the number of mounts performed during setup
} else { if e, a := config.expectedSetupMounts, len(mounter.Log); e != a {
if mounter.Log[0].Action != mount.FakeActionMount || mounter.Log[0].FSType != "tmpfs" { t.Errorf("Expected %v mounter calls during setup, got %v", e, a)
t.Errorf("Unexpected mounter action: %#v", mounter.Log[0]) } 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() 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 { if err != nil {
t.Errorf("Failed to make a new Cleaner: %v", err) 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") t.Errorf("Got a nil Cleaner")
} }
// Tear down the volume
if err := cleaner.TearDown(); err != nil { if err := cleaner.TearDown(); err != nil {
t.Errorf("Expected success, got: %v", err) t.Errorf("Expected success, got: %v", err)
} }
@ -183,18 +270,19 @@ func TestPluginTmpfs(t *testing.T) {
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
t.Errorf("SetUp() failed: %v", 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) // Check the number of mounter calls during tardown
} else { if e, a := config.expectedTeardownMounts, len(mounter.Log); e != a {
if mounter.Log[0].Action != mount.FakeActionUnmount { t.Errorf("Expected %v mounter calls during teardown, got %v", e, a)
t.Errorf("Unexpected mounter action: %#v", mounter.Log[0]) } else if config.expectedTeardownMounts == 1 && mounter.Log[0].Action != mount.FakeActionUnmount {
} t.Errorf("Unexpected mounter action during teardown: %#v", mounter.Log[0])
} }
mounter.ResetLog() mounter.ResetLog()
} }
func TestPluginBackCompat(t *testing.T) { 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{ spec := &api.Volume{
Name: "vol1", Name: "vol1",

View File

@ -18,7 +18,9 @@ limitations under the License.
package empty_dir package empty_dir
import "github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount" import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount"
)
// realMountDetector pretends to implement mediumer. // realMountDetector pretends to implement mediumer.
type realMountDetector struct { type realMountDetector struct {
@ -28,3 +30,7 @@ type realMountDetector struct {
func (m *realMountDetector) GetMountMedium(path string) (storageMedium, bool, error) { func (m *realMountDetector) GetMountMedium(path string) (storageMedium, bool, error) {
return mediumUnknown, false, nil return mediumUnknown, false, nil
} }
func selinuxEnabled() bool {
return false
}

View File

@ -27,51 +27,182 @@ import (
. "github.com/onsi/ginkgo" . "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() { var _ = Describe("EmptyDir volumes", func() {
f := NewFramework("emptydir") f := NewFramework("emptydir")
It("should have the correct mode", func() { It("volume on tmpfs should have the correct mode", func() {
volumePath := "/test-volume" doTestVolumeMode(f, testImageRootUid, api.StorageMediumMemory)
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("should support r/w", func() { It("should support (root,0644,tmpfs)", func() {
volumePath := "/test-volume" doTest0644(f, testImageRootUid, api.StorageMediumMemory)
filePath := path.Join(volumePath, "test-file") })
source := &api.EmptyDirVolumeSource{
Medium: api.StorageMediumMemory,
}
pod := testPodWithVolume(volumePath, source)
pod.Spec.Containers[0].Args = []string{ It("should support (root,0666,tmpfs)", func() {
fmt.Sprintf("--fs_type=%v", volumePath), doTest0666(f, testImageRootUid, api.StorageMediumMemory)
fmt.Sprintf("--rw_new_file=%v", filePath), })
fmt.Sprintf("--file_mode=%v", filePath),
} It("should support (root,0777,tmpfs)", func() {
f.TestContainerOutput("emptydir r/w on tmpfs", pod, 0, []string{ doTest0777(f, testImageRootUid, api.StorageMediumMemory)
"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 (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 (
const volumeName = "test-volume" 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()) podName := "pod-" + string(util.NewUUID())
return &api.Pod{ return &api.Pod{
@ -86,7 +217,7 @@ func testPodWithVolume(path string, source *api.EmptyDirVolumeSource) *api.Pod {
Containers: []api.Container{ Containers: []api.Container{
{ {
Name: containerName, Name: containerName,
Image: "gcr.io/google_containers/mounttest:0.2", Image: image,
VolumeMounts: []api.VolumeMount{ VolumeMounts: []api.VolumeMount{
{ {
Name: volumeName, Name: volumeName,