Merge pull request #5166 from thockin/tmpfs

Add tmpfs support as a flag on emptyDir
This commit is contained in:
Victor Marmol 2015-03-13 14:48:52 -07:00
commit efcde725cc
10 changed files with 378 additions and 50 deletions

View File

@ -182,12 +182,29 @@ type VolumeSource struct {
Secret *SecretVolumeSource `json:"secret"`
}
// HostPathVolumeSource represents bare host directory volume.
// HostPathVolumeSource represents a host directory mapped into a pod.
type HostPathVolumeSource struct {
Path string `json:"path"`
}
type EmptyDirVolumeSource struct{}
// EmptyDirVolumeSource represents an empty directory for a pod.
type EmptyDirVolumeSource struct {
// TODO: Longer term we want to represent the selection of underlying
// media more like a scheduling problem - user says what traits they
// need, we give them a backing store that satisifies that. For now
// this will cover the most common needs.
// Optional: what type of storage medium should back this directory.
// The default is "" which means to use the node's default medium.
Medium StorageType `json:"medium"`
}
// StorageType defines ways that storage can be allocated to a volume.
type StorageType string
const (
StorageTypeDefault StorageType = "" // use whatever the default is for the node
StorageTypeMemory StorageType = "Memory" // use memory (tmpfs)
)
// Protocol defines network protocols supported for things like conatiner ports.
type Protocol string

View File

@ -112,7 +112,19 @@ type HostPathVolumeSource struct {
Path string `json:"path" description:"path of the directory on the host"`
}
type EmptyDirVolumeSource struct{}
type EmptyDirVolumeSource struct {
// Optional: what type of storage medium should back this directory.
// The default is "" which means to use the node's default medium.
Medium StorageType `json:"medium" description:"type of storage used to back the volume; must be an empty string (default) or Memory"`
}
// StorageType defines ways that storage can be allocated to a volume.
type StorageType string
const (
StorageTypeDefault StorageType = "" // use whatever the default is for the node
StorageTypeMemory StorageType = "Memory" // use memory (tmpfs)
)
// Protocol defines network protocols supported for things like conatiner ports.
type Protocol string

View File

@ -90,7 +90,19 @@ type HostPathVolumeSource struct {
// Represents an empty directory volume.
//
// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/volumes.md#emptydir
type EmptyDirVolumeSource struct{}
type EmptyDirVolumeSource struct {
// Optional: what type of storage medium should back this directory.
// The default is "" which means to use the node's default medium.
Medium StorageType `json:"medium" description:"type of storage used to back the volume; must be an empty string (default) or Memory"`
}
// StorageType defines ways that storage can be allocated to a volume.
type StorageType string
const (
StorageTypeDefault StorageType = "" // use whatever the default is for the node
StorageTypeMemory StorageType = "Memory" // use memory (tmpfs)
)
// SecretVolumeSource adapts a Secret into a VolumeSource
//

View File

@ -206,7 +206,19 @@ type HostPathVolumeSource struct {
Path string `json:"path" description:"path of the directory on the host"`
}
type EmptyDirVolumeSource struct{}
type EmptyDirVolumeSource struct {
// Optional: what type of storage medium should back this directory.
// The default is "" which means to use the node's default medium.
Medium StorageType `json:"medium" description:"type of storage used to back the volume; must be an empty string (default) or Memory"`
}
// StorageType defines ways that storage can be allocated to a volume.
type StorageType string
const (
StorageTypeDefault StorageType = "" // use whatever the default is for the node
StorageTypeMemory StorageType = "Memory" // use memory (tmpfs)
)
// Protocol defines network protocols supported for things like conatiner ports.
type Protocol string

View File

@ -24,6 +24,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount"
)
// This is the primary entrypoint for volume plugins.
@ -70,26 +71,83 @@ func (plugin *emptyDirPlugin) CanSupport(spec *api.Volume) bool {
}
func (plugin *emptyDirPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
// Inject real implementations here, test through the internal function.
return plugin.newBuilderInternal(spec, podRef, mount.New(), &realMediumer{})
}
func (plugin *emptyDirPlugin) newBuilderInternal(spec *api.Volume, podRef *api.ObjectReference, mounter mount.Interface, mediumer mediumer) (volume.Builder, error) {
if plugin.legacyMode {
// Legacy mode instances can be cleaned up but not created anew.
return nil, fmt.Errorf("legacy mode: can not create new instances")
}
return &emptyDir{podRef.UID, spec.Name, plugin, false}, nil
medium := api.StorageTypeDefault
if spec.EmptyDir != nil { // Support a non-specified source as EmptyDir.
medium = spec.EmptyDir.Medium
}
return &emptyDir{
podUID: podRef.UID,
volName: spec.Name,
medium: medium,
mediumer: mediumer,
mounter: mounter,
plugin: plugin,
legacyMode: false,
}, nil
}
func (plugin *emptyDirPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) {
// Inject real implementations here, test through the internal function.
return plugin.newCleanerInternal(volName, podUID, mount.New(), &realMediumer{})
}
func (plugin *emptyDirPlugin) newCleanerInternal(volName string, podUID types.UID, mounter mount.Interface, mediumer mediumer) (volume.Cleaner, error) {
legacy := false
if plugin.legacyMode {
legacy = true
}
return &emptyDir{podUID, volName, plugin, legacy}, nil
ed := &emptyDir{
podUID: podUID,
volName: volName,
medium: api.StorageTypeDefault, // might be changed later
mounter: mounter,
mediumer: mediumer,
plugin: plugin,
legacyMode: legacy,
}
// Figure out the medium.
if medium, err := mediumer.GetMedium(ed.GetPath()); err != nil {
return nil, err
} else {
switch medium {
case mediumMemory:
ed.medium = api.StorageTypeMemory
default:
// assume StorageTypeDefault
}
}
return ed, nil
}
// mediumer abstracts how to find what storageMedium a path is backed by.
type mediumer interface {
GetMedium(path string) (storageMedium, error)
}
type storageMedium int
const (
mediumUnknown storageMedium = 0 // assume anything we don't explicitly handle is this
mediumMemory storageMedium = 1 // memory (e.g. tmpfs on linux)
)
// 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
volName string
medium api.StorageType
mounter mount.Interface
mediumer mediumer
plugin *emptyDirPlugin
legacyMode bool
}
@ -99,8 +157,34 @@ func (ed *emptyDir) SetUp() error {
if ed.legacyMode {
return fmt.Errorf("legacy mode: can not create new instances")
}
path := ed.GetPath()
return os.MkdirAll(path, 0750)
switch ed.medium {
case api.StorageTypeDefault:
return ed.setupDefault()
case api.StorageTypeMemory:
return ed.setupTmpfs()
default:
return fmt.Errorf("unknown storage medium %q", ed.medium)
}
}
func (ed *emptyDir) setupDefault() error {
return os.MkdirAll(ed.GetPath(), 0750)
}
func (ed *emptyDir) setupTmpfs() error {
if ed.mounter == nil {
return fmt.Errorf("memory storage requested, but mounter is nil")
}
if err := os.MkdirAll(ed.GetPath(), 0750); err != nil {
return err
}
// Make SetUp idempotent.
if medium, err := ed.mediumer.GetMedium(ed.GetPath()); err != nil {
return err
} else if medium == mediumMemory {
return nil // current state is what we expect
}
return ed.mounter.Mount("tmpfs", ed.GetPath(), "tmpfs", 0, "")
}
func (ed *emptyDir) GetPath() string {
@ -111,8 +195,19 @@ func (ed *emptyDir) GetPath() string {
return ed.plugin.host.GetPodVolumeDir(ed.podUID, volume.EscapePluginName(name), ed.volName)
}
// TearDown simply deletes everything in the directory.
// TearDown simply discards everything in the directory.
func (ed *emptyDir) TearDown() error {
switch ed.medium {
case api.StorageTypeDefault:
return ed.teardownDefault()
case api.StorageTypeMemory:
return ed.teardownTmpfs()
default:
return fmt.Errorf("unknown storage medium %q", ed.medium)
}
}
func (ed *emptyDir) teardownDefault() error {
tmpDir, err := volume.RenameDirectory(ed.GetPath(), ed.volName+".deleting~")
if err != nil {
return err
@ -123,3 +218,16 @@ func (ed *emptyDir) TearDown() error {
}
return nil
}
func (ed *emptyDir) teardownTmpfs() error {
if ed.mounter == nil {
return fmt.Errorf("memory storage requested, but mounter is nil")
}
if err := ed.mounter.Unmount(ed.GetPath(), 0); err != nil {
return err
}
if err := os.RemoveAll(ed.GetPath()); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,39 @@
/*
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.
*/
package empty_dir
import (
"fmt"
"syscall"
)
// Defined by Linux - the type number for tmpfs mounts.
const linuxTmpfsMagic = 0x01021994
// realMediumer implements mediumer in terms of syscalls.
type realMediumer struct{}
func (m *realMediumer) GetMedium(path string) (storageMedium, error) {
buf := syscall.Statfs_t{}
if err := syscall.Statfs(path, &buf); err != nil {
return 0, fmt.Errorf("statfs(%q): %v", path, err)
}
if buf.Type == linuxTmpfsMagic {
return mediumMemory, nil
}
return mediumUnknown, nil
}

View File

@ -18,21 +18,33 @@ package empty_dir
import (
"os"
"path"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount"
)
func TestCanSupport(t *testing.T) {
plugMgr := volume.PluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake", nil})
// The dir where volumes will be stored.
const basePath = "/tmp/fake"
plug, err := plugMgr.FindPluginByName("kubernetes.io/empty-dir")
// Construct an instance of a plugin, by name.
func makePluginUnderTest(t *testing.T, plugName string) volume.Plugin {
plugMgr := volume.PluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{basePath, nil})
plug, err := plugMgr.FindPluginByName(plugName)
if err != nil {
t.Errorf("Can't find the plugin by name")
}
return plug
}
func TestCanSupport(t *testing.T) {
plug := makePluginUnderTest(t, "kubernetes.io/empty-dir")
if plug.Name() != "kubernetes.io/empty-dir" {
t.Errorf("Wrong name: %s", plug.Name())
}
@ -44,19 +56,24 @@ func TestCanSupport(t *testing.T) {
}
}
func TestPlugin(t *testing.T) {
plugMgr := volume.PluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake", nil})
type fakeMediumer struct {
typeToReturn storageMedium
}
func (fake *fakeMediumer) GetMedium(path string) (storageMedium, error) {
return fake.typeToReturn, nil
}
func TestPlugin(t *testing.T) {
plug := makePluginUnderTest(t, "kubernetes.io/empty-dir")
plug, err := plugMgr.FindPluginByName("kubernetes.io/empty-dir")
if err != nil {
t.Errorf("Can't find the plugin by name")
}
spec := &api.Volume{
Name: "vol1",
VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}},
VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{Medium: api.StorageTypeDefault}},
}
builder, err := plug.NewBuilder(spec, &api.ObjectReference{UID: types.UID("poduid")})
mounter := mount.FakeMounter{}
mediumer := fakeMediumer{}
builder, err := plug.(*emptyDirPlugin).newBuilderInternal(spec, &api.ObjectReference{UID: types.UID("poduid")}, &mounter, &mediumer)
if err != nil {
t.Errorf("Failed to make a new Builder: %v", err)
}
@ -64,23 +81,27 @@ func TestPlugin(t *testing.T) {
t.Errorf("Got a nil Builder: %v")
}
path := builder.GetPath()
if path != "/tmp/fake/pods/poduid/volumes/kubernetes.io~empty-dir/vol1" {
t.Errorf("Got unexpected path: %s", path)
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(path); err != nil {
if _, err := os.Stat(volPath); err != nil {
if os.IsNotExist(err) {
t.Errorf("SetUp() failed, volume path not created: %s", path)
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.NewCleaner("vol1", types.UID("poduid"))
cleaner, err := plug.(*emptyDirPlugin).newCleanerInternal("vol1", types.UID("poduid"), &mounter, &fakeMediumer{})
if err != nil {
t.Errorf("Failed to make a new Cleaner: %v", err)
}
@ -91,21 +112,87 @@ func TestPlugin(t *testing.T) {
if err := cleaner.TearDown(); err != nil {
t.Errorf("Expected success, got: %v", err)
}
if _, err := os.Stat(path); err == nil {
t.Errorf("TearDown() failed, volume path still exists: %s", path)
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) {
plug := makePluginUnderTest(t, "kubernetes.io/empty-dir")
spec := &api.Volume{
Name: "vol1",
VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{Medium: api.StorageTypeMemory}},
}
mounter := mount.FakeMounter{}
mediumer := fakeMediumer{}
builder, err := plug.(*emptyDirPlugin).newBuilderInternal(spec, &api.ObjectReference{UID: types.UID("poduid")}, &mounter, &mediumer)
if err != nil {
t.Errorf("Failed to make a new Builder: %v", err)
}
if builder == nil {
t.Errorf("Got a nil Builder: %v")
}
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) != 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])
}
}
mounter.ResetLog()
cleaner, err := plug.(*emptyDirPlugin).newCleanerInternal("vol1", types.UID("poduid"), &mounter, &fakeMediumer{mediumMemory})
if err != nil {
t.Errorf("Failed to make a new Cleaner: %v", err)
}
if cleaner == nil {
t.Errorf("Got a nil Cleaner: %v")
}
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) != 1 {
t.Errorf("Expected 1 mounter call, got %#v", mounter.Log)
} else {
if mounter.Log[0].Action != mount.FakeActionUnmount {
t.Errorf("Unexpected mounter action: %#v", mounter.Log[0])
}
}
mounter.ResetLog()
}
func TestPluginBackCompat(t *testing.T) {
plugMgr := volume.PluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake", nil})
plug := makePluginUnderTest(t, "kubernetes.io/empty-dir")
plug, err := plugMgr.FindPluginByName("kubernetes.io/empty-dir")
if err != nil {
t.Errorf("Can't find the plugin by name")
}
spec := &api.Volume{
Name: "vol1",
}
@ -117,20 +204,15 @@ func TestPluginBackCompat(t *testing.T) {
t.Errorf("Got a nil Builder: %v")
}
path := builder.GetPath()
if path != "/tmp/fake/pods/poduid/volumes/kubernetes.io~empty-dir/vol1" {
t.Errorf("Got unexpected path: %s", path)
volPath := builder.GetPath()
if volPath != path.Join(basePath, "pods/poduid/volumes/kubernetes.io~empty-dir/vol1") {
t.Errorf("Got unexpected path: %s", volPath)
}
}
func TestPluginLegacy(t *testing.T) {
plugMgr := volume.PluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake", nil})
plug := makePluginUnderTest(t, "empty")
plug, err := plugMgr.FindPluginByName("empty")
if err != nil {
t.Errorf("Can't find the plugin by name")
}
if plug.Name() != "empty" {
t.Errorf("Wrong name: %s", plug.Name())
}
@ -138,11 +220,12 @@ func TestPluginLegacy(t *testing.T) {
t.Errorf("Expected false")
}
if _, err := plug.NewBuilder(&api.Volume{VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, &api.ObjectReference{UID: types.UID("poduid")}); err == nil {
spec := api.Volume{VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}
if _, err := plug.(*emptyDirPlugin).newBuilderInternal(&spec, &api.ObjectReference{UID: types.UID("poduid")}, &mount.FakeMounter{}, &fakeMediumer{}); err == nil {
t.Errorf("Expected failiure")
}
cleaner, err := plug.NewCleaner("vol1", types.UID("poduid"))
cleaner, err := plug.(*emptyDirPlugin).newCleanerInternal("vol1", types.UID("poduid"), &mount.FakeMounter{}, &fakeMediumer{})
if err != nil {
t.Errorf("Failed to make a new Cleaner: %v", err)
}

View File

@ -0,0 +1,26 @@
// +build !linux
/*
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.
*/
package empty_dir
// realMediumer pretends to implement mediumer.
type realMediumer struct{}
func (m *realMediumer) GetMedium(path string) (storageMedium, error) {
return mediumUnknown, nil
}

View File

@ -16,16 +16,35 @@ limitations under the License.
package mount
// FakeMounter implements mount.Interface.
// FakeMounter implements mount.Interface for tests.
type FakeMounter struct {
MountPoints []MountPoint
Log []FakeAction
}
// Values for FakeAction.Action
const FakeActionMount = "mount"
const FakeActionUnmount = "unmount"
// FakeAction objects are logged every time a fake mount or unmount is called.
type FakeAction struct {
Action string // "mount" or "unmount"
Target string // applies to both mount and unmount actions
Source string // applies only to "mount" actions
FSType string // applies only to "mount" actions
}
func (f *FakeMounter) ResetLog() {
f.Log = []FakeAction{}
}
func (f *FakeMounter) Mount(source string, target string, fstype string, flags uintptr, data string) error {
f.Log = append(f.Log, FakeAction{Action: FakeActionMount, Target: target, Source: source, FSType: fstype})
return nil
}
func (f *FakeMounter) Unmount(target string, flags int) error {
f.Log = append(f.Log, FakeAction{Action: FakeActionUnmount, Target: target})
return nil
}

View File

@ -94,7 +94,7 @@ func slicesEqual(a, b []string) bool {
func TestGetMountRefs(t *testing.T) {
fm := &FakeMounter{
[]MountPoint{
MountPoints: []MountPoint{
{Device: "/dev/sdb", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd"},
{Device: "/dev/sdb", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod"},
{Device: "/dev/sdc", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2"},