mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-09-24 19:38:02 +00:00
Implement volumes as plugins.
Break up the monolithic volumes code in kubelet into very small individual modules with a well-defined interface. Move them all into their own packages and beef up testing along the way.
This commit is contained in:
19
pkg/kubelet/volume/doc.go
Normal file
19
pkg/kubelet/volume/doc.go
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2014 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 volume includes internal representations of external volume types
|
||||
// as well as utility methods required to mount/unmount volumes to kubelets.
|
||||
package volume
|
125
pkg/kubelet/volume/empty_dir/empty_dir.go
Normal file
125
pkg/kubelet/volume/empty_dir/empty_dir.go
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
Copyright 2014 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"
|
||||
"os"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// This is the primary entrypoint for volume plugins.
|
||||
func ProbeVolumePlugins() []volume.Plugin {
|
||||
return []volume.Plugin{&emptyDirPlugin{nil, false}, &emptyDirPlugin{nil, true}}
|
||||
}
|
||||
|
||||
type emptyDirPlugin struct {
|
||||
host volume.Host
|
||||
legacyMode bool // if set, plugin answers to the legacy name
|
||||
}
|
||||
|
||||
var _ volume.Plugin = &emptyDirPlugin{}
|
||||
|
||||
const (
|
||||
emptyDirPluginName = "kubernetes.io/empty-dir"
|
||||
emptyDirPluginLegacyName = "empty"
|
||||
)
|
||||
|
||||
func (plugin *emptyDirPlugin) Init(host volume.Host) {
|
||||
plugin.host = host
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) Name() string {
|
||||
if plugin.legacyMode {
|
||||
return emptyDirPluginLegacyName
|
||||
}
|
||||
return emptyDirPluginName
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) CanSupport(spec *api.Volume) bool {
|
||||
if plugin.legacyMode {
|
||||
// Legacy mode instances can be cleaned up but not created anew.
|
||||
return false
|
||||
}
|
||||
|
||||
if spec.Source == nil || util.AllPtrFieldsNil(spec.Source) {
|
||||
return true
|
||||
}
|
||||
if spec.Source.EmptyDir != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) NewBuilder(spec *api.Volume, podUID types.UID) (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{podUID, spec.Name, plugin, false}, nil
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) {
|
||||
legacy := false
|
||||
if plugin.legacyMode {
|
||||
legacy = true
|
||||
}
|
||||
return &emptyDir{podUID, volName, plugin, legacy}, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
plugin *emptyDirPlugin
|
||||
legacyMode bool
|
||||
}
|
||||
|
||||
// SetUp creates new directory.
|
||||
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)
|
||||
}
|
||||
|
||||
func (ed *emptyDir) GetPath() string {
|
||||
name := emptyDirPluginName
|
||||
if ed.legacyMode {
|
||||
name = emptyDirPluginLegacyName
|
||||
}
|
||||
return ed.plugin.host.GetPodVolumeDir(ed.podUID, volume.EscapePluginName(name), ed.volName)
|
||||
}
|
||||
|
||||
// TearDown simply deletes everything in the directory.
|
||||
func (ed *emptyDir) TearDown() error {
|
||||
tmpDir, err := volume.RenameDirectory(ed.GetPath(), ed.volName+".deleting~")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.RemoveAll(tmpDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
153
pkg/kubelet/volume/empty_dir/empty_dir_test.go
Normal file
153
pkg/kubelet/volume/empty_dir/empty_dir_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
Copyright 2014 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 (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
|
||||
)
|
||||
|
||||
func TestCanSupport(t *testing.T) {
|
||||
plugMgr := volume.PluginMgr{}
|
||||
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"})
|
||||
|
||||
plug, err := plugMgr.FindPluginByName("kubernetes.io/empty-dir")
|
||||
if err != nil {
|
||||
t.Errorf("Can't find the plugin by name")
|
||||
}
|
||||
if plug.Name() != "kubernetes.io/empty-dir" {
|
||||
t.Errorf("Wrong name: %s", plug.Name())
|
||||
}
|
||||
if !plug.CanSupport(&api.Volume{Source: &api.VolumeSource{EmptyDir: &api.EmptyDir{}}}) {
|
||||
t.Errorf("Expected true")
|
||||
}
|
||||
if !plug.CanSupport(&api.Volume{Source: nil}) {
|
||||
t.Errorf("Expected true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
plugMgr := volume.PluginMgr{}
|
||||
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"})
|
||||
|
||||
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",
|
||||
Source: &api.VolumeSource{EmptyDir: &api.EmptyDir{}},
|
||||
}
|
||||
builder, err := plug.NewBuilder(spec, types.UID("poduid"))
|
||||
if err != nil {
|
||||
t.Errorf("Failed to make a new Builder: %v", err)
|
||||
}
|
||||
if builder == nil {
|
||||
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)
|
||||
}
|
||||
|
||||
if err := builder.SetUp(); err != nil {
|
||||
t.Errorf("Expected success, got: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Errorf("SetUp() failed, volume path not created: %s", path)
|
||||
} else {
|
||||
t.Errorf("SetUp() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
cleaner, err := plug.NewCleaner("vol1", types.UID("poduid"))
|
||||
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(path); err == nil {
|
||||
t.Errorf("TearDown() failed, volume path still exists: %s", path)
|
||||
} else if !os.IsNotExist(err) {
|
||||
t.Errorf("SetUp() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginBackCompat(t *testing.T) {
|
||||
plugMgr := volume.PluginMgr{}
|
||||
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"})
|
||||
|
||||
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",
|
||||
Source: nil,
|
||||
}
|
||||
builder, err := plug.NewBuilder(spec, types.UID("poduid"))
|
||||
if err != nil {
|
||||
t.Errorf("Failed to make a new Builder: %v", err)
|
||||
}
|
||||
if builder == nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginLegacy(t *testing.T) {
|
||||
plugMgr := volume.PluginMgr{}
|
||||
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"})
|
||||
|
||||
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())
|
||||
}
|
||||
if plug.CanSupport(&api.Volume{Source: &api.VolumeSource{EmptyDir: &api.EmptyDir{}}}) {
|
||||
t.Errorf("Expected false")
|
||||
}
|
||||
|
||||
if _, err := plug.NewBuilder(&api.Volume{Source: &api.VolumeSource{EmptyDir: &api.EmptyDir{}}}, types.UID("poduid")); err == nil {
|
||||
t.Errorf("Expected failiure")
|
||||
}
|
||||
|
||||
cleaner, err := plug.NewCleaner("vol1", types.UID("poduid"))
|
||||
if err != nil {
|
||||
t.Errorf("Failed to make a new Cleaner: %v", err)
|
||||
}
|
||||
if cleaner == nil {
|
||||
t.Errorf("Got a nil Cleaner: %v")
|
||||
}
|
||||
}
|
237
pkg/kubelet/volume/gce_pd/gce_pd.go
Normal file
237
pkg/kubelet/volume/gce_pd/gce_pd.go
Normal file
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
Copyright 2014 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 gce_pd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// This is the primary entrypoint for volume plugins.
|
||||
func ProbeVolumePlugins() []volume.Plugin {
|
||||
return []volume.Plugin{&gcePersistentDiskPlugin{nil, false}, &gcePersistentDiskPlugin{nil, true}}
|
||||
}
|
||||
|
||||
type gcePersistentDiskPlugin struct {
|
||||
host volume.Host
|
||||
legacyMode bool // if set, plugin answers to the legacy name
|
||||
}
|
||||
|
||||
var _ volume.Plugin = &gcePersistentDiskPlugin{}
|
||||
|
||||
const (
|
||||
gcePersistentDiskPluginName = "kubernetes.io/gce-pd"
|
||||
gcePersistentDiskPluginLegacyName = "gce-pd"
|
||||
)
|
||||
|
||||
func (plugin *gcePersistentDiskPlugin) Init(host volume.Host) {
|
||||
plugin.host = host
|
||||
}
|
||||
|
||||
func (plugin *gcePersistentDiskPlugin) Name() string {
|
||||
if plugin.legacyMode {
|
||||
return gcePersistentDiskPluginLegacyName
|
||||
}
|
||||
return gcePersistentDiskPluginName
|
||||
}
|
||||
|
||||
func (plugin *gcePersistentDiskPlugin) CanSupport(spec *api.Volume) bool {
|
||||
if plugin.legacyMode {
|
||||
// Legacy mode instances can be cleaned up but not created anew.
|
||||
return false
|
||||
}
|
||||
|
||||
if spec.Source != nil && spec.Source.GCEPersistentDisk != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (plugin *gcePersistentDiskPlugin) NewBuilder(spec *api.Volume, podUID types.UID) (volume.Builder, error) {
|
||||
// Inject real implementations here, test through the internal function.
|
||||
return plugin.newBuilderInternal(spec, podUID, &GCEDiskUtil{}, mount.New())
|
||||
}
|
||||
|
||||
func (plugin *gcePersistentDiskPlugin) newBuilderInternal(spec *api.Volume, podUID types.UID, manager pdManager, mounter mount.Interface) (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")
|
||||
}
|
||||
|
||||
pdName := spec.Source.GCEPersistentDisk.PDName
|
||||
fsType := spec.Source.GCEPersistentDisk.FSType
|
||||
partition := ""
|
||||
if spec.Source.GCEPersistentDisk.Partition != 0 {
|
||||
partition = strconv.Itoa(spec.Source.GCEPersistentDisk.Partition)
|
||||
}
|
||||
readOnly := spec.Source.GCEPersistentDisk.ReadOnly
|
||||
|
||||
return &gcePersistentDisk{
|
||||
podUID: podUID,
|
||||
volName: spec.Name,
|
||||
pdName: pdName,
|
||||
fsType: fsType,
|
||||
partition: partition,
|
||||
readOnly: readOnly,
|
||||
manager: manager,
|
||||
mounter: mounter,
|
||||
plugin: plugin,
|
||||
legacyMode: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (plugin *gcePersistentDiskPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) {
|
||||
// Inject real implementations here, test through the internal function.
|
||||
return plugin.newCleanerInternal(volName, podUID, &GCEDiskUtil{}, mount.New())
|
||||
}
|
||||
|
||||
func (plugin *gcePersistentDiskPlugin) newCleanerInternal(volName string, podUID types.UID, manager pdManager, mounter mount.Interface) (volume.Cleaner, error) {
|
||||
legacy := false
|
||||
if plugin.legacyMode {
|
||||
legacy = true
|
||||
}
|
||||
return &gcePersistentDisk{
|
||||
podUID: podUID,
|
||||
volName: volName,
|
||||
manager: manager,
|
||||
mounter: mounter,
|
||||
plugin: plugin,
|
||||
legacyMode: legacy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Abstract interface to PD operations.
|
||||
type pdManager interface {
|
||||
// Attaches the disk to the kubelet's host machine.
|
||||
AttachDisk(pd *gcePersistentDisk) error
|
||||
// Detaches the disk from the kubelet's host machine.
|
||||
DetachDisk(pd *gcePersistentDisk, devicePath string) error
|
||||
}
|
||||
|
||||
// gcePersistentDisk volumes are disk resources provided by Google Compute Engine
|
||||
// that are attached to the kubelet's host machine and exposed to the pod.
|
||||
type gcePersistentDisk struct {
|
||||
volName string
|
||||
podUID types.UID
|
||||
// Unique identifier of the PD, used to find the disk resource in the provider.
|
||||
pdName string
|
||||
// Filesystem type, optional.
|
||||
fsType string
|
||||
// Specifies the partition to mount
|
||||
partition string
|
||||
// Specifies whether the disk will be attached as read-only.
|
||||
readOnly bool
|
||||
// Utility interface that provides API calls to the provider to attach/detach disks.
|
||||
manager pdManager
|
||||
// Mounter interface that provides system calls to mount the disks.
|
||||
mounter mount.Interface
|
||||
plugin *gcePersistentDiskPlugin
|
||||
legacyMode bool
|
||||
}
|
||||
|
||||
// SetUp attaches the disk and bind mounts to the volume path.
|
||||
func (pd *gcePersistentDisk) SetUp() error {
|
||||
if pd.legacyMode {
|
||||
return fmt.Errorf("legacy mode: can not create new instances")
|
||||
}
|
||||
|
||||
// TODO: handle failed mounts here.
|
||||
mountpoint, err := isMountPoint(pd.GetPath())
|
||||
glog.V(4).Infof("PersistentDisk set up: %s %v %v", pd.GetPath(), mountpoint, err)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if mountpoint {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := pd.manager.AttachDisk(pd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
flags := uintptr(0)
|
||||
if pd.readOnly {
|
||||
flags = mount.FlagReadOnly
|
||||
}
|
||||
|
||||
volPath := pd.GetPath()
|
||||
if err := os.MkdirAll(volPath, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Perform a bind mount to the full path to allow duplicate mounts of the same PD.
|
||||
globalPDPath := makeGlobalPDName(pd.plugin.host, pd.pdName, pd.readOnly)
|
||||
err = pd.mounter.Mount(globalPDPath, pd.GetPath(), "", mount.FlagBind|flags, "")
|
||||
if err != nil {
|
||||
os.RemoveAll(pd.GetPath())
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeGlobalPDName(host volume.Host, devName string, readOnly bool) string {
|
||||
return path.Join(host.GetPluginDir(gcePersistentDiskPluginName), "mounts", devName)
|
||||
}
|
||||
|
||||
func (pd *gcePersistentDisk) GetPath() string {
|
||||
name := gcePersistentDiskPluginName
|
||||
if pd.legacyMode {
|
||||
name = gcePersistentDiskPluginLegacyName
|
||||
}
|
||||
return pd.plugin.host.GetPodVolumeDir(pd.podUID, volume.EscapePluginName(name), pd.volName)
|
||||
}
|
||||
|
||||
// Unmounts the bind mount, and detaches the disk only if the PD
|
||||
// resource was the last reference to that disk on the kubelet.
|
||||
func (pd *gcePersistentDisk) TearDown() error {
|
||||
mountpoint, err := isMountPoint(pd.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !mountpoint {
|
||||
return os.RemoveAll(pd.GetPath())
|
||||
}
|
||||
|
||||
devicePath, refCount, err := getMountRefCount(pd.mounter, pd.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pd.mounter.Unmount(pd.GetPath(), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
refCount--
|
||||
if err := os.RemoveAll(pd.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
// If refCount is 1, then all bind mounts have been removed, and the
|
||||
// remaining reference is the global mount. It is safe to detach.
|
||||
if refCount == 1 {
|
||||
if err := pd.manager.DetachDisk(pd, devicePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
173
pkg/kubelet/volume/gce_pd/gce_pd_test.go
Normal file
173
pkg/kubelet/volume/gce_pd/gce_pd_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
Copyright 2014 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 gce_pd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"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"})
|
||||
|
||||
plug, err := plugMgr.FindPluginByName("kubernetes.io/gce-pd")
|
||||
if err != nil {
|
||||
t.Errorf("Can't find the plugin by name")
|
||||
}
|
||||
if plug.Name() != "kubernetes.io/gce-pd" {
|
||||
t.Errorf("Wrong name: %s", plug.Name())
|
||||
}
|
||||
if !plug.CanSupport(&api.Volume{Source: &api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDisk{}}}) {
|
||||
t.Errorf("Expected true")
|
||||
}
|
||||
}
|
||||
|
||||
type fakePDManager struct{}
|
||||
|
||||
// TODO(jonesdl) To fully test this, we could create a loopback device
|
||||
// and mount that instead.
|
||||
func (fake *fakePDManager) AttachDisk(pd *gcePersistentDisk) error {
|
||||
globalPath := makeGlobalPDName(pd.plugin.host, pd.pdName, pd.readOnly)
|
||||
err := os.MkdirAll(globalPath, 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fake *fakePDManager) DetachDisk(pd *gcePersistentDisk, devicePath string) error {
|
||||
globalPath := makeGlobalPDName(pd.plugin.host, pd.pdName, pd.readOnly)
|
||||
err := os.RemoveAll(globalPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeMounter struct{}
|
||||
|
||||
func (fake *fakeMounter) Mount(source string, target string, fstype string, flags uintptr, data string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fake *fakeMounter) Unmount(target string, flags int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fake *fakeMounter) List() ([]mount.MountPoint, error) {
|
||||
return []mount.MountPoint{}, nil
|
||||
}
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
plugMgr := volume.PluginMgr{}
|
||||
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"})
|
||||
|
||||
plug, err := plugMgr.FindPluginByName("kubernetes.io/gce-pd")
|
||||
if err != nil {
|
||||
t.Errorf("Can't find the plugin by name")
|
||||
}
|
||||
spec := &api.Volume{
|
||||
Name: "vol1",
|
||||
Source: &api.VolumeSource{
|
||||
GCEPersistentDisk: &api.GCEPersistentDisk{
|
||||
PDName: "pd",
|
||||
FSType: "ext4",
|
||||
},
|
||||
},
|
||||
}
|
||||
builder, err := plug.(*gcePersistentDiskPlugin).newBuilderInternal(spec, types.UID("poduid"), &fakePDManager{}, &fakeMounter{})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to make a new Builder: %v", err)
|
||||
}
|
||||
if builder == nil {
|
||||
t.Errorf("Got a nil Builder: %v")
|
||||
}
|
||||
|
||||
path := builder.GetPath()
|
||||
if path != "/tmp/fake/pods/poduid/volumes/kubernetes.io~gce-pd/vol1" {
|
||||
t.Errorf("Got unexpected path: %s", path)
|
||||
}
|
||||
|
||||
if err := builder.SetUp(); err != nil {
|
||||
t.Errorf("Expected success, got: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Errorf("SetUp() failed, volume path not created: %s", path)
|
||||
} else {
|
||||
t.Errorf("SetUp() failed: %v", err)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Errorf("SetUp() failed, volume path not created: %s", path)
|
||||
} else {
|
||||
t.Errorf("SetUp() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
cleaner, err := plug.(*gcePersistentDiskPlugin).newCleanerInternal("vol1", types.UID("poduid"), &fakePDManager{}, &fakeMounter{})
|
||||
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(path); err == nil {
|
||||
t.Errorf("TearDown() failed, volume path still exists: %s", path)
|
||||
} else if !os.IsNotExist(err) {
|
||||
t.Errorf("SetUp() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginLegacy(t *testing.T) {
|
||||
plugMgr := volume.PluginMgr{}
|
||||
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"})
|
||||
|
||||
plug, err := plugMgr.FindPluginByName("gce-pd")
|
||||
if err != nil {
|
||||
t.Errorf("Can't find the plugin by name")
|
||||
}
|
||||
if plug.Name() != "gce-pd" {
|
||||
t.Errorf("Wrong name: %s", plug.Name())
|
||||
}
|
||||
if plug.CanSupport(&api.Volume{Source: &api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDisk{}}}) {
|
||||
t.Errorf("Expected false")
|
||||
}
|
||||
|
||||
if _, err := plug.NewBuilder(&api.Volume{Source: &api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDisk{}}}, types.UID("poduid")); err == nil {
|
||||
t.Errorf("Expected failiure")
|
||||
}
|
||||
|
||||
cleaner, err := plug.NewCleaner("vol1", types.UID("poduid"))
|
||||
if err != nil {
|
||||
t.Errorf("Failed to make a new Cleaner: %v", err)
|
||||
}
|
||||
if cleaner == nil {
|
||||
t.Errorf("Got a nil Cleaner: %v")
|
||||
}
|
||||
}
|
141
pkg/kubelet/volume/gce_pd/gce_util.go
Normal file
141
pkg/kubelet/volume/gce_pd/gce_util.go
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
Copyright 2014 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 gce_pd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount"
|
||||
)
|
||||
|
||||
const partitionRegex = "[a-z][a-z]*(?P<partition>[0-9][0-9]*)?"
|
||||
|
||||
var regexMatcher = regexp.MustCompile(partitionRegex)
|
||||
|
||||
type GCEDiskUtil struct{}
|
||||
|
||||
// Attaches a disk specified by a volume.GCEPersistentDisk to the current kubelet.
|
||||
// Mounts the disk to it's global path.
|
||||
func (util *GCEDiskUtil) AttachDisk(pd *gcePersistentDisk) error {
|
||||
gce, err := cloudprovider.GetCloudProvider("gce", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
flags := uintptr(0)
|
||||
if pd.readOnly {
|
||||
flags = mount.FlagReadOnly
|
||||
}
|
||||
if err := gce.(*gce_cloud.GCECloud).AttachDisk(pd.pdName, pd.readOnly); err != nil {
|
||||
return err
|
||||
}
|
||||
devicePath := path.Join("/dev/disk/by-id/", "google-"+pd.pdName)
|
||||
if pd.partition != "" {
|
||||
devicePath = devicePath + "-part" + pd.partition
|
||||
}
|
||||
//TODO(jonesdl) There should probably be better method than busy-waiting here.
|
||||
numTries := 0
|
||||
for {
|
||||
_, err := os.Stat(devicePath)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
numTries++
|
||||
if numTries == 10 {
|
||||
return errors.New("Could not attach disk: Timeout after 10s")
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
globalPDPath := makeGlobalPDName(pd.plugin.host, pd.pdName, pd.readOnly)
|
||||
// Only mount the PD globally once.
|
||||
mountpoint, err := isMountPoint(globalPDPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(globalPDPath, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
mountpoint = false
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !mountpoint {
|
||||
err = pd.mounter.Mount(devicePath, globalPDPath, pd.fsType, flags, "")
|
||||
if err != nil {
|
||||
os.RemoveAll(globalPDPath)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDeviceName(devicePath, canonicalDevicePath string) (string, error) {
|
||||
isMatch := regexMatcher.MatchString(path.Base(canonicalDevicePath))
|
||||
if !isMatch {
|
||||
return "", fmt.Errorf("unexpected device: %s", canonicalDevicePath)
|
||||
}
|
||||
if isMatch {
|
||||
result := make(map[string]string)
|
||||
substrings := regexMatcher.FindStringSubmatch(path.Base(canonicalDevicePath))
|
||||
for i, label := range regexMatcher.SubexpNames() {
|
||||
result[label] = substrings[i]
|
||||
}
|
||||
partition := result["partition"]
|
||||
devicePath = strings.TrimSuffix(devicePath, "-part"+partition)
|
||||
}
|
||||
return strings.TrimPrefix(path.Base(devicePath), "google-"), nil
|
||||
}
|
||||
|
||||
// Unmounts the device and detaches the disk from the kubelet's host machine.
|
||||
// Expects a GCE device path symlink. Ex: /dev/disk/by-id/google-mydisk-part1
|
||||
func (util *GCEDiskUtil) DetachDisk(pd *gcePersistentDisk, devicePath string) error {
|
||||
// Follow the symlink to the actual device path.
|
||||
canonicalDevicePath, err := filepath.EvalSymlinks(devicePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deviceName, err := getDeviceName(devicePath, canonicalDevicePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globalPDPath := makeGlobalPDName(pd.plugin.host, deviceName, pd.readOnly)
|
||||
if err := pd.mounter.Unmount(globalPDPath, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.RemoveAll(globalPDPath); err != nil {
|
||||
return err
|
||||
}
|
||||
gce, err := cloudprovider.GetCloudProvider("gce", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gce.(*gce_cloud.GCECloud).DetachDisk(deviceName); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
55
pkg/kubelet/volume/gce_pd/gce_util_test.go
Normal file
55
pkg/kubelet/volume/gce_pd/gce_util_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2014 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 gce_pd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetDeviceName(t *testing.T) {
|
||||
tests := []struct {
|
||||
deviceName string
|
||||
canonicalName string
|
||||
expectedName string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
deviceName: "/dev/google-sd0-part0",
|
||||
canonicalName: "/dev/google/sd0P1",
|
||||
expectedName: "sd0",
|
||||
},
|
||||
{
|
||||
canonicalName: "0123456",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
name, err := getDeviceName(test.deviceName, test.canonicalName)
|
||||
if test.expectError {
|
||||
if err == nil {
|
||||
t.Error("unexpected non-error")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if name != test.expectedName {
|
||||
t.Errorf("expected: %s, got %s", test.expectedName, name)
|
||||
}
|
||||
}
|
||||
}
|
53
pkg/kubelet/volume/gce_pd/mount_util.go
Normal file
53
pkg/kubelet/volume/gce_pd/mount_util.go
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright 2014 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 gce_pd
|
||||
|
||||
import (
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount"
|
||||
)
|
||||
|
||||
// Examines /proc/mounts to find the source device of the PD resource and the
|
||||
// number of references to that device. Returns both the full device path under
|
||||
// the /dev tree and the number of references.
|
||||
func getMountRefCount(mounter mount.Interface, mountPath string) (string, int, error) {
|
||||
// TODO(jonesdl) This can be split up into two procedures, finding the device path
|
||||
// and finding the number of references. The parsing could also be separated and another
|
||||
// utility could determine if a path is an active mount point.
|
||||
|
||||
mps, err := mounter.List()
|
||||
if err != nil {
|
||||
return "", -1, err
|
||||
}
|
||||
|
||||
// Find the device name.
|
||||
deviceName := ""
|
||||
for i := range mps {
|
||||
if mps[i].Path == mountPath {
|
||||
deviceName = mps[i].Device
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Find the number of references to the device.
|
||||
refCount := 0
|
||||
for i := range mps {
|
||||
if mps[i].Device == deviceName {
|
||||
refCount++
|
||||
}
|
||||
}
|
||||
return deviceName, refCount, nil
|
||||
}
|
40
pkg/kubelet/volume/gce_pd/mount_util_linux.go
Normal file
40
pkg/kubelet/volume/gce_pd/mount_util_linux.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// +build !windows
|
||||
|
||||
/*
|
||||
Copyright 2014 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 gce_pd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Determine if a directory is a mountpoint, by comparing the device for the directory
|
||||
// with the device for it's parent. If they are the same, it's not a mountpoint, if they're
|
||||
// different, it is.
|
||||
func isMountPoint(file string) (bool, error) {
|
||||
stat, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
rootStat, err := os.Lstat(file + "/..")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// If the directory has the same device as parent, then it's not a mountpoint.
|
||||
return stat.Sys().(*syscall.Stat_t).Dev != rootStat.Sys().(*syscall.Stat_t).Dev, nil
|
||||
}
|
28
pkg/kubelet/volume/gce_pd/mount_util_unsupported.go
Normal file
28
pkg/kubelet/volume/gce_pd/mount_util_unsupported.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// +build !linux
|
||||
|
||||
/*
|
||||
Copyright 2014 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 gce_pd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Dummy implementation for Windows
|
||||
func isMountPoint(file string) (bool, error) {
|
||||
return false, fmt.Errorf("unimplemented")
|
||||
}
|
214
pkg/kubelet/volume/git_repo/git_repo.go
Normal file
214
pkg/kubelet/volume/git_repo/git_repo.go
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
Copyright 2014 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 git_repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"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/exec"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// This is the primary entrypoint for volume plugins.
|
||||
func ProbeVolumePlugins() []volume.Plugin {
|
||||
return []volume.Plugin{&gitRepoPlugin{nil, false}, &gitRepoPlugin{nil, true}}
|
||||
}
|
||||
|
||||
type gitRepoPlugin struct {
|
||||
host volume.Host
|
||||
legacyMode bool // if set, plugin answers to the legacy name
|
||||
}
|
||||
|
||||
var _ volume.Plugin = &gitRepoPlugin{}
|
||||
|
||||
const (
|
||||
gitRepoPluginName = "kubernetes.io/git-repo"
|
||||
gitRepoPluginLegacyName = "git"
|
||||
)
|
||||
|
||||
func (plugin *gitRepoPlugin) Init(host volume.Host) {
|
||||
plugin.host = host
|
||||
}
|
||||
|
||||
func (plugin *gitRepoPlugin) Name() string {
|
||||
if plugin.legacyMode {
|
||||
return gitRepoPluginLegacyName
|
||||
}
|
||||
return gitRepoPluginName
|
||||
}
|
||||
|
||||
func (plugin *gitRepoPlugin) CanSupport(spec *api.Volume) bool {
|
||||
if plugin.legacyMode {
|
||||
// Legacy mode instances can be cleaned up but not created anew.
|
||||
return false
|
||||
}
|
||||
|
||||
if spec.Source != nil && spec.Source.GitRepo != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (plugin *gitRepoPlugin) NewBuilder(spec *api.Volume, podUID types.UID) (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 &gitRepo{
|
||||
podUID: podUID,
|
||||
volName: spec.Name,
|
||||
source: spec.Source.GitRepo.Repository,
|
||||
revision: spec.Source.GitRepo.Revision,
|
||||
exec: exec.New(),
|
||||
plugin: plugin,
|
||||
legacyMode: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (plugin *gitRepoPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) {
|
||||
legacy := false
|
||||
if plugin.legacyMode {
|
||||
legacy = true
|
||||
}
|
||||
return &gitRepo{
|
||||
podUID: podUID,
|
||||
volName: volName,
|
||||
plugin: plugin,
|
||||
legacyMode: legacy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// gitRepo volumes are directories which are pre-filled from a git repository.
|
||||
// These do not persist beyond the lifetime of a pod.
|
||||
type gitRepo struct {
|
||||
volName string
|
||||
podUID types.UID
|
||||
source string
|
||||
revision string
|
||||
exec exec.Interface
|
||||
plugin *gitRepoPlugin
|
||||
legacyMode bool
|
||||
}
|
||||
|
||||
// SetUp creates new directory and clones a git repo.
|
||||
func (gr *gitRepo) SetUp() error {
|
||||
if gr.isReady() {
|
||||
return nil
|
||||
}
|
||||
if gr.legacyMode {
|
||||
return fmt.Errorf("legacy mode: can not create new instances")
|
||||
}
|
||||
|
||||
volPath := gr.GetPath()
|
||||
if err := os.MkdirAll(volPath, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if output, err := gr.execCommand("git", []string{"clone", gr.source}, gr.GetPath()); err != nil {
|
||||
return fmt.Errorf("failed to exec 'git clone %s': %s: %v", gr.source, output, err)
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(gr.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(files) != 1 {
|
||||
return fmt.Errorf("unexpected directory contents: %v", files)
|
||||
}
|
||||
if len(gr.revision) == 0 {
|
||||
// Done!
|
||||
gr.setReady()
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := path.Join(gr.GetPath(), files[0].Name())
|
||||
if output, err := gr.execCommand("git", []string{"checkout", gr.revision}, dir); err != nil {
|
||||
return fmt.Errorf("failed to exec 'git checkout %s': %s: %v", gr.revision, output, err)
|
||||
}
|
||||
if output, err := gr.execCommand("git", []string{"reset", "--hard"}, dir); err != nil {
|
||||
return fmt.Errorf("failed to exec 'git reset --hard': %s: %v", output, err)
|
||||
}
|
||||
|
||||
gr.setReady()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gr *gitRepo) getMetaDir() string {
|
||||
return path.Join(gr.plugin.host.GetPodPluginDir(gr.podUID, volume.EscapePluginName(gitRepoPluginName)), gr.volName)
|
||||
}
|
||||
|
||||
func (gr *gitRepo) isReady() bool {
|
||||
metaDir := gr.getMetaDir()
|
||||
readyFile := path.Join(metaDir, "ready")
|
||||
s, err := os.Stat(readyFile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !s.Mode().IsRegular() {
|
||||
glog.Errorf("GitRepo ready-file is not a file: %s", readyFile)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (gr *gitRepo) setReady() {
|
||||
metaDir := gr.getMetaDir()
|
||||
if err := os.MkdirAll(metaDir, 0750); err != nil && !os.IsExist(err) {
|
||||
glog.Errorf("Can't mkdir %s: %v", metaDir, err)
|
||||
return
|
||||
}
|
||||
readyFile := path.Join(metaDir, "ready")
|
||||
file, err := os.Create(readyFile)
|
||||
if err != nil {
|
||||
glog.Errorf("Can't touch %s: %v", readyFile, err)
|
||||
return
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
func (gr *gitRepo) execCommand(command string, args []string, dir string) ([]byte, error) {
|
||||
cmd := gr.exec.Command(command, args...)
|
||||
cmd.SetDir(dir)
|
||||
return cmd.CombinedOutput()
|
||||
}
|
||||
|
||||
func (gr *gitRepo) GetPath() string {
|
||||
name := gitRepoPluginName
|
||||
if gr.legacyMode {
|
||||
name = gitRepoPluginLegacyName
|
||||
}
|
||||
return gr.plugin.host.GetPodVolumeDir(gr.podUID, volume.EscapePluginName(name), gr.volName)
|
||||
}
|
||||
|
||||
// TearDown simply deletes everything in the directory.
|
||||
func (gr *gitRepo) TearDown() error {
|
||||
tmpDir, err := volume.RenameDirectory(gr.GetPath(), gr.volName+".deleting~")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.RemoveAll(tmpDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
186
pkg/kubelet/volume/git_repo/git_repo_test.go
Normal file
186
pkg/kubelet/volume/git_repo/git_repo_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
Copyright 2014 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 git_repo
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"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/exec"
|
||||
)
|
||||
|
||||
func newTestHost(t *testing.T) volume.Host {
|
||||
tempDir, err := ioutil.TempDir("/tmp", "git_repo_test.")
|
||||
if err != nil {
|
||||
t.Fatalf("can't make a temp rootdir: %v", err)
|
||||
}
|
||||
return &volume.FakeHost{tempDir}
|
||||
}
|
||||
|
||||
func TestCanSupport(t *testing.T) {
|
||||
plugMgr := volume.PluginMgr{}
|
||||
plugMgr.InitPlugins(ProbeVolumePlugins(), newTestHost(t))
|
||||
|
||||
plug, err := plugMgr.FindPluginByName("kubernetes.io/git-repo")
|
||||
if err != nil {
|
||||
t.Errorf("Can't find the plugin by name")
|
||||
}
|
||||
if plug.Name() != "kubernetes.io/git-repo" {
|
||||
t.Errorf("Wrong name: %s", plug.Name())
|
||||
}
|
||||
if !plug.CanSupport(&api.Volume{Source: &api.VolumeSource{GitRepo: &api.GitRepo{}}}) {
|
||||
t.Errorf("Expected true")
|
||||
}
|
||||
}
|
||||
|
||||
func testSetUp(plug volume.Plugin, builder volume.Builder, t *testing.T) {
|
||||
var fcmd exec.FakeCmd
|
||||
fcmd = exec.FakeCmd{
|
||||
CombinedOutputScript: []exec.FakeCombinedOutputAction{
|
||||
// git clone
|
||||
func() ([]byte, error) {
|
||||
os.MkdirAll(path.Join(fcmd.Dirs[0], "kubernetes"), 0750)
|
||||
return []byte{}, nil
|
||||
},
|
||||
// git checkout
|
||||
func() ([]byte, error) { return []byte{}, nil },
|
||||
// git reset
|
||||
func() ([]byte, error) { return []byte{}, nil },
|
||||
},
|
||||
}
|
||||
fake := exec.FakeExec{
|
||||
CommandScript: []exec.FakeCommandAction{
|
||||
func(cmd string, args ...string) exec.Cmd { return exec.InitFakeCmd(&fcmd, cmd, args...) },
|
||||
func(cmd string, args ...string) exec.Cmd { return exec.InitFakeCmd(&fcmd, cmd, args...) },
|
||||
func(cmd string, args ...string) exec.Cmd { return exec.InitFakeCmd(&fcmd, cmd, args...) },
|
||||
},
|
||||
}
|
||||
g := builder.(*gitRepo)
|
||||
g.exec = &fake
|
||||
|
||||
err := g.SetUp()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
expectedCmds := [][]string{
|
||||
{"git", "clone", g.source},
|
||||
{"git", "checkout", g.revision},
|
||||
{"git", "reset", "--hard"},
|
||||
}
|
||||
if fake.CommandCalls != len(expectedCmds) {
|
||||
t.Errorf("unexpected command calls: expected 3, saw: %d", fake.CommandCalls)
|
||||
}
|
||||
if !reflect.DeepEqual(expectedCmds, fcmd.CombinedOutputLog) {
|
||||
t.Errorf("unexpected commands: %v, expected: %v", fcmd.CombinedOutputLog, expectedCmds)
|
||||
}
|
||||
expectedDirs := []string{g.GetPath(), g.GetPath() + "/kubernetes", g.GetPath() + "/kubernetes"}
|
||||
if len(fcmd.Dirs) != 3 || !reflect.DeepEqual(expectedDirs, fcmd.Dirs) {
|
||||
t.Errorf("unexpected directories: %v, expected: %v", fcmd.Dirs, expectedDirs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
plugMgr := volume.PluginMgr{}
|
||||
plugMgr.InitPlugins(ProbeVolumePlugins(), newTestHost(t))
|
||||
|
||||
plug, err := plugMgr.FindPluginByName("kubernetes.io/git-repo")
|
||||
if err != nil {
|
||||
t.Errorf("Can't find the plugin by name")
|
||||
}
|
||||
spec := &api.Volume{
|
||||
Name: "vol1",
|
||||
Source: &api.VolumeSource{
|
||||
GitRepo: &api.GitRepo{
|
||||
Repository: "https://github.com/GoogleCloudPlatform/kubernetes.git",
|
||||
Revision: "2a30ce65c5ab586b98916d83385c5983edd353a1",
|
||||
},
|
||||
},
|
||||
}
|
||||
builder, err := plug.NewBuilder(spec, types.UID("poduid"))
|
||||
if err != nil {
|
||||
t.Errorf("Failed to make a new Builder: %v", err)
|
||||
}
|
||||
if builder == nil {
|
||||
t.Errorf("Got a nil Builder: %v")
|
||||
}
|
||||
|
||||
path := builder.GetPath()
|
||||
if !strings.HasSuffix(path, "pods/poduid/volumes/kubernetes.io~git-repo/vol1") {
|
||||
t.Errorf("Got unexpected path: %s", path)
|
||||
}
|
||||
|
||||
testSetUp(plug, builder, t)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Errorf("SetUp() failed, volume path not created: %s", path)
|
||||
} else {
|
||||
t.Errorf("SetUp() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
cleaner, err := plug.NewCleaner("vol1", types.UID("poduid"))
|
||||
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(path); err == nil {
|
||||
t.Errorf("TearDown() failed, volume path still exists: %s", path)
|
||||
} else if !os.IsNotExist(err) {
|
||||
t.Errorf("SetUp() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginLegacy(t *testing.T) {
|
||||
plugMgr := volume.PluginMgr{}
|
||||
plugMgr.InitPlugins(ProbeVolumePlugins(), newTestHost(t))
|
||||
|
||||
plug, err := plugMgr.FindPluginByName("git")
|
||||
if err != nil {
|
||||
t.Errorf("Can't find the plugin by name")
|
||||
}
|
||||
if plug.Name() != "git" {
|
||||
t.Errorf("Wrong name: %s", plug.Name())
|
||||
}
|
||||
if plug.CanSupport(&api.Volume{Source: &api.VolumeSource{GitRepo: &api.GitRepo{}}}) {
|
||||
t.Errorf("Expected false")
|
||||
}
|
||||
|
||||
if _, err := plug.NewBuilder(&api.Volume{Source: &api.VolumeSource{GitRepo: &api.GitRepo{}}}, types.UID("poduid")); err == nil {
|
||||
t.Errorf("Expected failiure")
|
||||
}
|
||||
|
||||
cleaner, err := plug.NewCleaner("vol1", types.UID("poduid"))
|
||||
if err != nil {
|
||||
t.Errorf("Failed to make a new Cleaner: %v", err)
|
||||
}
|
||||
if cleaner == nil {
|
||||
t.Errorf("Got a nil Cleaner: %v")
|
||||
}
|
||||
}
|
81
pkg/kubelet/volume/host_path/host_path.go
Normal file
81
pkg/kubelet/volume/host_path/host_path.go
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright 2014 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 host_path
|
||||
|
||||
import (
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
|
||||
)
|
||||
|
||||
// This is the primary entrypoint for volume plugins.
|
||||
func ProbeVolumePlugins() []volume.Plugin {
|
||||
return []volume.Plugin{&hostPathPlugin{nil}}
|
||||
}
|
||||
|
||||
type hostPathPlugin struct {
|
||||
host volume.Host
|
||||
}
|
||||
|
||||
var _ volume.Plugin = &hostPathPlugin{}
|
||||
|
||||
const (
|
||||
hostPathPluginName = "kubernetes.io/host-path"
|
||||
)
|
||||
|
||||
func (plugin *hostPathPlugin) Init(host volume.Host) {
|
||||
plugin.host = host
|
||||
}
|
||||
|
||||
func (plugin *hostPathPlugin) Name() string {
|
||||
return hostPathPluginName
|
||||
}
|
||||
|
||||
func (plugin *hostPathPlugin) CanSupport(spec *api.Volume) bool {
|
||||
if spec.Source != nil && spec.Source.HostDir != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (plugin *hostPathPlugin) NewBuilder(spec *api.Volume, podUID types.UID) (volume.Builder, error) {
|
||||
return &hostPath{spec.Source.HostDir.Path}, nil
|
||||
}
|
||||
|
||||
func (plugin *hostPathPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) {
|
||||
return &hostPath{""}, nil
|
||||
}
|
||||
|
||||
// HostPath volumes represent a bare host file or directory mount.
|
||||
// The direct at the specified path will be directly exposed to the container.
|
||||
type hostPath struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// SetUp does nothing.
|
||||
func (hp *hostPath) SetUp() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hp *hostPath) GetPath() string {
|
||||
return hp.path
|
||||
}
|
||||
|
||||
// TearDown does nothing.
|
||||
func (hp *hostPath) TearDown() error {
|
||||
return nil
|
||||
}
|
86
pkg/kubelet/volume/host_path/host_path_test.go
Normal file
86
pkg/kubelet/volume/host_path/host_path_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright 2014 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 host_path
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
|
||||
)
|
||||
|
||||
func TestCanSupport(t *testing.T) {
|
||||
plugMgr := volume.PluginMgr{}
|
||||
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"fake"})
|
||||
|
||||
plug, err := plugMgr.FindPluginByName("kubernetes.io/host-path")
|
||||
if err != nil {
|
||||
t.Errorf("Can't find the plugin by name")
|
||||
}
|
||||
if plug.Name() != "kubernetes.io/host-path" {
|
||||
t.Errorf("Wrong name: %s", plug.Name())
|
||||
}
|
||||
if !plug.CanSupport(&api.Volume{Source: &api.VolumeSource{HostDir: &api.HostDir{}}}) {
|
||||
t.Errorf("Expected true")
|
||||
}
|
||||
if plug.CanSupport(&api.Volume{Source: nil}) {
|
||||
t.Errorf("Expected false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
plugMgr := volume.PluginMgr{}
|
||||
plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"fake"})
|
||||
|
||||
plug, err := plugMgr.FindPluginByName("kubernetes.io/host-path")
|
||||
if err != nil {
|
||||
t.Errorf("Can't find the plugin by name")
|
||||
}
|
||||
spec := &api.Volume{
|
||||
Name: "vol1",
|
||||
Source: &api.VolumeSource{HostDir: &api.HostDir{"/vol1"}},
|
||||
}
|
||||
builder, err := plug.NewBuilder(spec, types.UID("poduid"))
|
||||
if err != nil {
|
||||
t.Errorf("Failed to make a new Builder: %v", err)
|
||||
}
|
||||
if builder == nil {
|
||||
t.Errorf("Got a nil Builder: %v")
|
||||
}
|
||||
|
||||
path := builder.GetPath()
|
||||
if path != "/vol1" {
|
||||
t.Errorf("Got unexpected path: %s", path)
|
||||
}
|
||||
|
||||
if err := builder.SetUp(); err != nil {
|
||||
t.Errorf("Expected success, got: %v", err)
|
||||
}
|
||||
|
||||
cleaner, err := plug.NewCleaner("vol1", types.UID("poduid"))
|
||||
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)
|
||||
}
|
||||
}
|
174
pkg/kubelet/volume/plugins.go
Normal file
174
pkg/kubelet/volume/plugins.go
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright 2014 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 volume
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// Plugin is an interface to volume plugins.
|
||||
type Plugin interface {
|
||||
// Init initializes the plugin. This will be called exactly once
|
||||
// before any New* calls are made - implementations of plugins may
|
||||
// depend on this.
|
||||
Init(host Host)
|
||||
|
||||
// Name returns the plugin's name. Plugins should use namespaced names
|
||||
// such as "example.com/volume". The "kubernetes.io" namespace is
|
||||
// reserved for plugins which are bundled with kubernetes.
|
||||
Name() string
|
||||
|
||||
// CanSupport tests whether the Plugin supports a given volume
|
||||
// specification from the API. The spec pointer should be considered
|
||||
// const.
|
||||
CanSupport(spec *api.Volume) bool
|
||||
|
||||
// NewBuilder creates a new volume.Builder from an API specification.
|
||||
// Ownership of the spec pointer in *not* transferred.
|
||||
// - spec: The api.Volume spec
|
||||
// - podUID: The UID of the enclosing pod
|
||||
NewBuilder(spec *api.Volume, podUID types.UID) (Builder, error)
|
||||
|
||||
// NewCleaner creates a new volume.Cleaner from recoverable state.
|
||||
// - name: The volume name, as per the api.Volume spec.
|
||||
// - podUID: The UID of the enclosing pod
|
||||
NewCleaner(name string, podUID types.UID) (Cleaner, error)
|
||||
}
|
||||
|
||||
// Host is an interface that plugins can use to access the kubelet.
|
||||
type Host interface {
|
||||
// GetPluginDir returns the absolute path to a directory under which
|
||||
// a given plugin may store data. This directory might not actually
|
||||
// exist on disk yet. For plugin data that is per-pod, see
|
||||
// GetPodPluginDir().
|
||||
GetPluginDir(pluginName string) string
|
||||
|
||||
// GetPodVolumeDir returns the absolute path a directory which
|
||||
// represents the named volume under the named plugin for the given
|
||||
// pod. If the specified pod does not exist, the result of this call
|
||||
// might not exist.
|
||||
GetPodVolumeDir(podUID types.UID, pluginName string, volumeName string) string
|
||||
|
||||
// GetPodPluginDir returns the absolute path to a directory under which
|
||||
// a given plugin may store data for a given pod. If the specified pod
|
||||
// does not exist, the result of this call might not exist. This
|
||||
// directory might not actually exist on disk yet.
|
||||
GetPodPluginDir(podUID types.UID, pluginName string) string
|
||||
}
|
||||
|
||||
// PluginMgr tracks registered plugins.
|
||||
type PluginMgr struct {
|
||||
mutex sync.Mutex
|
||||
plugins map[string]Plugin
|
||||
}
|
||||
|
||||
// InitPlugins initializes each plugin. All plugins must have unique names.
|
||||
// This must be called exactly once before any New* methods are called on any
|
||||
// plugins.
|
||||
func (pm *PluginMgr) InitPlugins(plugins []Plugin, host Host) error {
|
||||
pm.mutex.Lock()
|
||||
defer pm.mutex.Unlock()
|
||||
|
||||
if pm.plugins == nil {
|
||||
pm.plugins = map[string]Plugin{}
|
||||
}
|
||||
|
||||
allErrs := []error{}
|
||||
for _, plugin := range plugins {
|
||||
name := plugin.Name()
|
||||
if !util.IsQualifiedName(name) {
|
||||
allErrs = append(allErrs, fmt.Errorf("volume plugin has invalid name: %#v", plugin))
|
||||
continue
|
||||
}
|
||||
|
||||
if _, found := pm.plugins[name]; found {
|
||||
allErrs = append(allErrs, fmt.Errorf("volume plugin %q was registered more than once", name))
|
||||
continue
|
||||
}
|
||||
plugin.Init(host)
|
||||
pm.plugins[name] = plugin
|
||||
glog.V(1).Infof("Loaded volume plugin %q", name)
|
||||
}
|
||||
return errors.NewAggregate(allErrs)
|
||||
}
|
||||
|
||||
// FindPluginBySpec looks for a plugin that can support a given volume
|
||||
// specification. If no plugins can support or more than one plugin can
|
||||
// support it, return error.
|
||||
func (pm *PluginMgr) FindPluginBySpec(spec *api.Volume) (Plugin, error) {
|
||||
pm.mutex.Lock()
|
||||
defer pm.mutex.Unlock()
|
||||
|
||||
matches := []string{}
|
||||
for k, v := range pm.plugins {
|
||||
if v.CanSupport(spec) {
|
||||
matches = append(matches, k)
|
||||
}
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return nil, fmt.Errorf("no volume plugin matched")
|
||||
}
|
||||
if len(matches) > 1 {
|
||||
return nil, fmt.Errorf("multiple volume plugins matched: %s", strings.Join(matches, ","))
|
||||
}
|
||||
return pm.plugins[matches[0]], nil
|
||||
}
|
||||
|
||||
// FindPluginByName fetches a plugin by name or by legacy name. If no plugin
|
||||
// is found, returns error.
|
||||
func (pm *PluginMgr) FindPluginByName(name string) (Plugin, error) {
|
||||
pm.mutex.Lock()
|
||||
defer pm.mutex.Unlock()
|
||||
|
||||
// Once we can get rid of legacy names we can reduce this to a map lookup.
|
||||
matches := []string{}
|
||||
for k, v := range pm.plugins {
|
||||
if v.Name() == name {
|
||||
matches = append(matches, k)
|
||||
}
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return nil, fmt.Errorf("no volume plugin matched")
|
||||
}
|
||||
if len(matches) > 1 {
|
||||
return nil, fmt.Errorf("multiple volume plugins matched: %s", strings.Join(matches, ","))
|
||||
}
|
||||
return pm.plugins[matches[0]], nil
|
||||
}
|
||||
|
||||
// EscapePluginName converts a plugin name, which might contain a / into a
|
||||
// string that is safe to use on-disk. This assumes that the input has already
|
||||
// been validates as a qualified name. we use "~" rather than ":" here in case
|
||||
// we ever use a filesystem that doesn't allow ":".
|
||||
func EscapePluginName(in string) string {
|
||||
return strings.Replace(in, "/", "~", -1)
|
||||
}
|
||||
|
||||
// UnescapePluginName converts an escaped plugin name (as per EscapePluginName)
|
||||
// back to its normal form. This assumes that the input has already been
|
||||
// validates as a qualified name.
|
||||
func UnescapePluginName(in string) string {
|
||||
return strings.Replace(in, "~", "/", -1)
|
||||
}
|
92
pkg/kubelet/volume/testing.go
Normal file
92
pkg/kubelet/volume/testing.go
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
Copyright 2014 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 volume
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
|
||||
)
|
||||
|
||||
// FakeHost is useful for testing volume plugins.
|
||||
type FakeHost struct {
|
||||
RootDir string
|
||||
}
|
||||
|
||||
func (f *FakeHost) GetPluginDir(podUID string) string {
|
||||
return path.Join(f.RootDir, "plugins", podUID)
|
||||
}
|
||||
|
||||
func (f *FakeHost) GetPodVolumeDir(podUID types.UID, pluginName, volumeName string) string {
|
||||
return path.Join(f.RootDir, "pods", string(podUID), "volumes", pluginName, volumeName)
|
||||
}
|
||||
|
||||
func (f *FakeHost) GetPodPluginDir(podUID types.UID, pluginName string) string {
|
||||
return path.Join(f.RootDir, "pods", string(podUID), "plugins", pluginName)
|
||||
}
|
||||
|
||||
// FakePlugin is useful for for testing. It tries to be a fully compliant
|
||||
// plugin, but all it does is make empty directories.
|
||||
// Use as:
|
||||
// volume.RegisterPlugin(&FakePlugin{"fake-name"})
|
||||
type FakePlugin struct {
|
||||
PluginName string
|
||||
Host Host
|
||||
}
|
||||
|
||||
var _ Plugin = &FakePlugin{}
|
||||
|
||||
func (plugin *FakePlugin) Init(host Host) {
|
||||
plugin.Host = host
|
||||
}
|
||||
|
||||
func (plugin *FakePlugin) Name() string {
|
||||
return plugin.PluginName
|
||||
}
|
||||
|
||||
func (plugin *FakePlugin) CanSupport(spec *api.Volume) bool {
|
||||
// TODO: maybe pattern-match on spec.Name to decide?
|
||||
return true
|
||||
}
|
||||
|
||||
func (plugin *FakePlugin) NewBuilder(spec *api.Volume, podUID types.UID) (Builder, error) {
|
||||
return &FakeVolume{podUID, spec.Name, plugin}, nil
|
||||
}
|
||||
|
||||
func (plugin *FakePlugin) NewCleaner(volName string, podUID types.UID) (Cleaner, error) {
|
||||
return &FakeVolume{podUID, volName, plugin}, nil
|
||||
}
|
||||
|
||||
type FakeVolume struct {
|
||||
PodUID types.UID
|
||||
VolName string
|
||||
Plugin *FakePlugin
|
||||
}
|
||||
|
||||
func (fv *FakeVolume) SetUp() error {
|
||||
return os.MkdirAll(fv.GetPath(), 0750)
|
||||
}
|
||||
|
||||
func (fv *FakeVolume) GetPath() string {
|
||||
return path.Join(fv.Plugin.Host.GetPodVolumeDir(fv.PodUID, EscapePluginName(fv.Plugin.PluginName), fv.VolName))
|
||||
}
|
||||
|
||||
func (fv *FakeVolume) TearDown() error {
|
||||
return os.RemoveAll(fv.GetPath())
|
||||
}
|
59
pkg/kubelet/volume/volume.go
Normal file
59
pkg/kubelet/volume/volume.go
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2014 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 volume
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
// Interface is a directory used by pods or hosts.
|
||||
// All method implementations of methods in the volume interface must be idempotent.
|
||||
type Interface interface {
|
||||
// GetPath returns the directory path the volume is mounted to.
|
||||
GetPath() string
|
||||
}
|
||||
|
||||
// Builder interface provides method to set up/mount the volume.
|
||||
type Builder interface {
|
||||
// Uses Interface to provide the path for Docker binds.
|
||||
Interface
|
||||
// SetUp prepares and mounts/unpacks the volume to a directory path.
|
||||
// This may be called more than once, so implementations must be
|
||||
// idempotent.
|
||||
SetUp() error
|
||||
}
|
||||
|
||||
// Cleaner interface provides method to cleanup/unmount the volumes.
|
||||
type Cleaner interface {
|
||||
Interface
|
||||
// TearDown unmounts the volume and removes traces of the SetUp procedure.
|
||||
TearDown() error
|
||||
}
|
||||
|
||||
func RenameDirectory(oldPath, newName string) (string, error) {
|
||||
newPath, err := ioutil.TempDir(path.Dir(oldPath), newName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = os.Rename(oldPath, newPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return newPath, nil
|
||||
}
|
Reference in New Issue
Block a user