Add tmpfs support to EmptyDir

# *** ERROR: *** docs are out of sync between cli and markdown
# run hack/run-gendocs.sh > docs/kubectl.md to regenerate

#
# Your commit will be aborted unless you regenerate docs.
    COMMIT_BLOCKED_ON_GENDOCS
This commit is contained in:
Tim Hockin 2015-03-07 12:35:00 -08:00
parent 50c96789e7
commit caca5e7358
8 changed files with 357 additions and 48 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
}