diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/cmd/kube-controller-manager/app/plugins.go b/Godeps/_workspace/src/k8s.io/kubernetes/cmd/kube-controller-manager/app/plugins.go index c68e54ad89b7..61cc057e7361 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/cmd/kube-controller-manager/app/plugins.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/cmd/kube-controller-manager/app/plugins.go @@ -63,6 +63,10 @@ func ProbeRecyclableVolumePlugins(flags VolumeConfigFlags) []volume.VolumePlugin } allPlugins = append(allPlugins, host_path.ProbeVolumePlugins(hostPathConfig)...) + allPlugins = append(allPlugins, cinder.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, aws_ebs.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, gce_pd.ProbeVolumePlugins()...) + nfsConfig := volume.VolumeConfig{ RecyclerMinimumTimeout: flags.PersistentVolumeRecyclerMinimumTimeoutNFS, RecyclerTimeoutIncrement: flags.PersistentVolumeRecyclerIncrementTimeoutNFS, diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/cloudprovider/providers/gce/gce.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/cloudprovider/providers/gce/gce.go index c6987de80307..d3143561096e 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/cloudprovider/providers/gce/gce.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/cloudprovider/providers/gce/gce.go @@ -816,6 +816,25 @@ func (gce *GCECloud) DetachDisk(devicePath string) error { return err } +func (gce *GCECloud) CreateDisk(name string, sizeGB int64) error { + insertOp, err := gce.service.Disks.Insert(gce.projectID, gce.zone, &compute.Disk{ + Name: name, + SizeGb: sizeGB, + }).Do() + if err != nil { + return err + } + return gce.waitForZoneOp(insertOp) +} + +func (gce *GCECloud) DeleteDisk(name string) error { + deleteOp, err := gce.service.Disks.Delete(gce.projectID, gce.zone, name).Do() + if err != nil { + return err + } + return gce.waitForZoneOp(deleteOp) +} + func (gce *GCECloud) getDisk(diskName string) (*compute.Disk, error) { return gce.service.Disks.Get(gce.projectID, gce.zone, diskName).Do() } diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/cloudprovider/providers/openstack/openstack.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/cloudprovider/providers/openstack/openstack.go index f5f814b83218..bb3b6d1ce988 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/cloudprovider/providers/openstack/openstack.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/cloudprovider/providers/openstack/openstack.go @@ -921,3 +921,41 @@ func (os *OpenStack) getComputeIDbyHostname(cClient *gophercloud.ServiceClient) } return "", fmt.Errorf("No server found matching hostname: %s", hostname) } + +// Create a volume of given size (in GiB) +func (os *OpenStack) CreateVolume(size int) (volumeName string, err error) { + + sClient, err := openstack.NewBlockStorageV1(os.provider, gophercloud.EndpointOpts{ + Region: os.region, + }) + + if err != nil || sClient == nil { + glog.Errorf("Unable to initialize cinder client for region: %s", os.region) + return "", err + } + + opts := volumes.CreateOpts{Size: size} + vol, err := volumes.Create(sClient, opts).Extract() + if err != nil { + glog.Errorf("Failed to create a %d GB volume: %v", size, err) + return "", err + } + glog.Infof("Created volume %v", vol.ID) + return vol.ID, err +} + +func (os *OpenStack) DeleteVolume(volumeName string) error { + sClient, err := openstack.NewBlockStorageV1(os.provider, gophercloud.EndpointOpts{ + Region: os.region, + }) + + if err != nil || sClient == nil { + glog.Errorf("Unable to initialize cinder client for region: %s", os.region) + return err + } + err = volumes.Delete(sClient, volumeName).ExtractErr() + if err != nil { + glog.Errorf("Cannot delete volume %s: %v", volumeName, err) + } + return err +} diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/cloudprovider/providers/openstack/openstack_test.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/cloudprovider/providers/openstack/openstack_test.go index 0c5667676161..be70f496d024 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/cloudprovider/providers/openstack/openstack_test.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/cloudprovider/providers/openstack/openstack_test.go @@ -198,3 +198,26 @@ func TestZones(t *testing.T) { t.Fatalf("GetZone() returned wrong region (%s)", zone.Region) } } + +func TestVolumes(t *testing.T) { + cfg, ok := configFromEnv() + if !ok { + t.Skipf("No config found in environment") + } + + os, err := newOpenStack(cfg) + if err != nil { + t.Fatalf("Failed to construct/authenticate OpenStack: %s", err) + } + + vol, err := os.CreateVolume(1) + if err != nil { + t.Fatalf("Cannot create a new Cinder volume: %v", err) + } + + err = os.DeleteVolume(vol) + if err != nil { + t.Fatalf("Cannot delete Cinder volume %s: %v", vol, err) + } + +} diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/aws_ebs/aws_ebs.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/aws_ebs/aws_ebs.go index ecada1a28af6..8c4ca0f9e196 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/aws_ebs/aws_ebs.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/aws_ebs/aws_ebs.go @@ -26,6 +26,7 @@ import ( "github.com/golang/glog" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/cloudprovider" "k8s.io/kubernetes/pkg/cloudprovider/providers/aws" "k8s.io/kubernetes/pkg/types" @@ -46,6 +47,8 @@ type awsElasticBlockStorePlugin struct { var _ volume.VolumePlugin = &awsElasticBlockStorePlugin{} var _ volume.PersistentVolumePlugin = &awsElasticBlockStorePlugin{} +var _ volume.DeletableVolumePlugin = &awsElasticBlockStorePlugin{} +var _ volume.ProvisionableVolumePlugin = &awsElasticBlockStorePlugin{} const ( awsElasticBlockStorePluginName = "kubernetes.io/aws-ebs" @@ -125,12 +128,50 @@ func (plugin *awsElasticBlockStorePlugin) newCleanerInternal(volName string, pod }}, nil } +func (plugin *awsElasticBlockStorePlugin) NewDeleter(spec *volume.Spec) (volume.Deleter, error) { + return plugin.newDeleterInternal(spec, &AWSDiskUtil{}) +} + +func (plugin *awsElasticBlockStorePlugin) newDeleterInternal(spec *volume.Spec, manager ebsManager) (volume.Deleter, error) { + if spec.PersistentVolume != nil && spec.PersistentVolume.Spec.AWSElasticBlockStore == nil { + return nil, fmt.Errorf("spec.PersistentVolumeSource.AWSElasticBlockStore is nil") + } + return &awsElasticBlockStoreDeleter{ + awsElasticBlockStore: &awsElasticBlockStore{ + volName: spec.Name(), + volumeID: spec.PersistentVolume.Spec.AWSElasticBlockStore.VolumeID, + manager: manager, + plugin: plugin, + }}, nil +} + +func (plugin *awsElasticBlockStorePlugin) NewCreater(options volume.VolumeOptions) (volume.Creater, error) { + if len(options.AccessModes) == 0 { + options.AccessModes = plugin.GetAccessModes() + } + return plugin.newCreaterInternal(options, &AWSDiskUtil{}) +} + +func (plugin *awsElasticBlockStorePlugin) newCreaterInternal(options volume.VolumeOptions, manager ebsManager) (volume.Creater, error) { + return &awsElasticBlockStoreCreater{ + awsElasticBlockStore: &awsElasticBlockStore{ + manager: manager, + plugin: plugin, + }, + options: options, + }, nil +} + // Abstract interface to PD operations. type ebsManager interface { // Attaches the disk to the kubelet's host machine. AttachAndMountDisk(b *awsElasticBlockStoreBuilder, globalPDPath string) error // Detaches the disk from the kubelet's host machine. DetachDisk(c *awsElasticBlockStoreCleaner) error + // Creates a volume + CreateVolume(creater *awsElasticBlockStoreCreater) (string, int, error) + // Deletes a volume + DeleteVolume(deleter *awsElasticBlockStoreDeleter) error } // awsElasticBlockStore volumes are disk resources provided by Google Compute Engine @@ -348,3 +389,66 @@ func (c *awsElasticBlockStoreCleaner) TearDownAt(dir string) error { } return nil } + +type awsElasticBlockStoreDeleter struct { + *awsElasticBlockStore +} + +var _ volume.Deleter = &awsElasticBlockStoreDeleter{} + +func (d *awsElasticBlockStoreDeleter) GetPath() string { + name := awsElasticBlockStorePluginName + return d.plugin.host.GetPodVolumeDir(d.podUID, util.EscapeQualifiedNameForDisk(name), d.volName) +} + +func (d *awsElasticBlockStoreDeleter) Delete() error { + return d.manager.DeleteVolume(d) +} + +type awsElasticBlockStoreCreater struct { + *awsElasticBlockStore + options volume.VolumeOptions +} + +var _ volume.Creater = &awsElasticBlockStoreCreater{} + +func (c *awsElasticBlockStoreCreater) Provision(pv *api.PersistentVolume) error { + volumeID, size, err := c.manager.CreateVolume(c) + if err != nil { + return err + } + pv.Spec.PersistentVolumeSource.AWSElasticBlockStore.VolumeID = volumeID + pv.Spec.Capacity = api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse(fmt.Sprintf("%d", size)), + } + return nil +} + +func (c *awsElasticBlockStoreCreater) NewPersistentVolumeTemplate() (*api.PersistentVolume, error) { + // Provide dummy api.PersistentVolume.Spec, it will be filled in + // awsElasticBlockStoreCreater.Provision() + return &api.PersistentVolume{ + ObjectMeta: api.ObjectMeta{ + GenerateName: "pv-aws-", + Labels: map[string]string{ + "createdby": "aws_ebs_dynamic_provisioner", + }, + Annotations: map[string]string{}, + }, + Spec: api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: c.options.PersistentVolumeReclaimPolicy, + AccessModes: c.options.AccessModes, + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): c.options.Capacity, + }, + PersistentVolumeSource: api.PersistentVolumeSource{ + AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{ + VolumeID: "dummy", + FSType: "ext4", + Partition: 0, + ReadOnly: false, + }, + }, + }, + }, nil +} diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/aws_ebs/aws_ebs_test.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/aws_ebs/aws_ebs_test.go index 517934b8fc48..69277d24d8ee 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/aws_ebs/aws_ebs_test.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/aws_ebs/aws_ebs_test.go @@ -94,6 +94,17 @@ func (fake *fakePDManager) DetachDisk(c *awsElasticBlockStoreCleaner) error { return nil } +func (fake *fakePDManager) CreateVolume(c *awsElasticBlockStoreCreater) (string, int, error) { + return "test-aws-volume-name", 100 * 1024 * 1024, nil +} + +func (fake *fakePDManager) DeleteVolume(cd *awsElasticBlockStoreDeleter) error { + if cd.volumeID != "test-aws-volume-name" { + return fmt.Errorf("Deleter got unexpected volume name: %s", cd.volumeID) + } + return nil +} + func TestPlugin(t *testing.T) { plugMgr := volume.VolumePluginMgr{} plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost("/tmp/fake", nil, nil)) @@ -158,6 +169,47 @@ func TestPlugin(t *testing.T) { } else if !os.IsNotExist(err) { t.Errorf("SetUp() failed: %v", err) } + + // Test Creater + cap := resource.MustParse("100Mi") + options := volume.VolumeOptions{ + Capacity: cap, + AccessModes: []api.PersistentVolumeAccessMode{ + api.ReadWriteOnce, + }, + PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimDelete, + } + creater, err := plug.(*awsElasticBlockStorePlugin).newCreaterInternal(options, &fakePDManager{}) + persistentSpec, err := creater.NewPersistentVolumeTemplate() + if err != nil { + t.Errorf("NewPersistentVolumeTemplate() failed: %v", err) + } + + // get 2nd Creater - persistent volume controller will do the same + provisioner, err := plug.(*awsElasticBlockStorePlugin).newCreaterInternal(options, &fakePDManager{}) + err = provisioner.Provision(persistentSpec) + if err != nil { + t.Errorf("Provision() failed: %v", err) + } + + if persistentSpec.Spec.PersistentVolumeSource.AWSElasticBlockStore.VolumeID != "test-aws-volume-name" { + t.Errorf("Provision() returned unexpected volume ID: %s", persistentSpec.Spec.PersistentVolumeSource.AWSElasticBlockStore.VolumeID) + } + cap = persistentSpec.Spec.Capacity[api.ResourceStorage] + size := cap.Value() + if size != 100*1024*1024 { + t.Errorf("Provision() returned unexpected volume size: %v", size) + } + + // Test Deleter + volSpec := &volume.Spec{ + PersistentVolume: persistentSpec, + } + deleter, err := plug.(*awsElasticBlockStorePlugin).newDeleterInternal(volSpec, &fakePDManager{}) + err = deleter.Delete() + if err != nil { + t.Errorf("Deleter() failed: %v", err) + } } func TestPersistentClaimReadOnlyFlag(t *testing.T) { diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/aws_ebs/aws_util.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/aws_ebs/aws_util.go index 6af88d1a1dc1..69f39770a4c4 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/aws_ebs/aws_util.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/aws_ebs/aws_util.go @@ -22,6 +22,7 @@ import ( "time" "github.com/golang/glog" + "k8s.io/kubernetes/pkg/cloudprovider/providers/aws" ) type AWSDiskUtil struct{} @@ -107,3 +108,40 @@ func (util *AWSDiskUtil) DetachDisk(c *awsElasticBlockStoreCleaner) error { } return nil } + +func (util *AWSDiskUtil) DeleteVolume(d *awsElasticBlockStoreDeleter) error { + cloud := d.plugin.host.GetCloudProvider() + if cloud == nil { + glog.Errorf("Cloud provider not initialized properly") + return errors.New("Cloud provider not initialized properly") + } + + if err := cloud.(*aws_cloud.AWSCloud).DeleteVolume(d.volumeID); err != nil { + glog.V(2).Infof("Error deleting AWS EBS volume %s: %v", d.volumeID, err) + return err + } + glog.V(2).Infof("Successfully deleted AWS EBS volume %s", d.volumeID) + return nil +} + +func (util *AWSDiskUtil) CreateVolume(c *awsElasticBlockStoreCreater) (string, int, error) { + const mega = 1024 * 1024 + cloud := c.plugin.host.GetCloudProvider() + if cloud == nil { + glog.Errorf("Cloud provider not initialized properly") + return "", 0, errors.New("Cloud provider not initialized properly") + } + + // convert to MB with rounding + volSize := int((c.options.Capacity.Value() + mega - 1) / mega) + volSpec := &aws_cloud.VolumeOptions{ + CapacityMB: volSize, + } + name, err := cloud.(*aws_cloud.AWSCloud).CreateVolume(volSpec) + if err != nil { + glog.V(2).Infof("Error creating AWS EBS volume: %v", err) + return "", 0, err + } + glog.V(2).Infof("Successfully created AWS EBS volume %s", name) + return name, volSize * mega, nil +} diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/cinder/cinder.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/cinder/cinder.go index 34f47af2de4a..1d52511e5689 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/cinder/cinder.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/cinder/cinder.go @@ -17,11 +17,13 @@ limitations under the License. package cinder import ( + "fmt" "os" "path" "github.com/golang/glog" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util/exec" @@ -39,6 +41,9 @@ type cinderPlugin struct { } var _ volume.VolumePlugin = &cinderPlugin{} +var _ volume.PersistentVolumePlugin = &cinderPlugin{} +var _ volume.DeletableVolumePlugin = &cinderPlugin{} +var _ volume.ProvisionableVolumePlugin = &cinderPlugin{} const ( cinderVolumePluginName = "kubernetes.io/cinder" @@ -107,12 +112,50 @@ func (plugin *cinderPlugin) newCleanerInternal(volName string, podUID types.UID, }}, nil } +func (plugin *cinderPlugin) NewDeleter(spec *volume.Spec) (volume.Deleter, error) { + return plugin.newDeleterInternal(spec, &CinderDiskUtil{}) +} + +func (plugin *cinderPlugin) newDeleterInternal(spec *volume.Spec, manager cdManager) (volume.Deleter, error) { + if spec.PersistentVolume != nil && spec.PersistentVolume.Spec.Cinder == nil { + return nil, fmt.Errorf("spec.PersistentVolumeSource.Cinder is nil") + } + return &cinderVolumeDeleter{ + &cinderVolume{ + volName: spec.Name(), + pdName: spec.PersistentVolume.Spec.Cinder.VolumeID, + manager: manager, + plugin: plugin, + }}, nil +} + +func (plugin *cinderPlugin) NewCreater(options volume.VolumeOptions) (volume.Creater, error) { + if len(options.AccessModes) == 0 { + options.AccessModes = plugin.GetAccessModes() + } + return plugin.newCreaterInternal(options, &CinderDiskUtil{}) +} + +func (plugin *cinderPlugin) newCreaterInternal(options volume.VolumeOptions, manager cdManager) (volume.Creater, error) { + return &cinderVolumeCreater{ + cinderVolume: &cinderVolume{ + manager: manager, + plugin: plugin, + }, + options: options, + }, nil +} + // Abstract interface to PD operations. type cdManager interface { // Attaches the disk to the kubelet's host machine. AttachDisk(builder *cinderVolumeBuilder, globalPDPath string) error // Detaches the disk from the kubelet's host machine. DetachDisk(cleaner *cinderVolumeCleaner) error + // Creates a volume + CreateVolume(creater *cinderVolumeCreater) (string, int, error) + // Deletes a volume + DeleteVolume(deleter *cinderVolumeDeleter) error } var _ volume.Builder = &cinderVolumeBuilder{} @@ -279,3 +322,66 @@ func (c *cinderVolumeCleaner) TearDownAt(dir string) error { } return nil } + +type cinderVolumeDeleter struct { + *cinderVolume +} + +var _ volume.Deleter = &cinderVolumeDeleter{} + +func (r *cinderVolumeDeleter) GetPath() string { + name := cinderVolumePluginName + return r.plugin.host.GetPodVolumeDir(r.podUID, util.EscapeQualifiedNameForDisk(name), r.volName) +} + +func (r *cinderVolumeDeleter) Delete() error { + return r.manager.DeleteVolume(r) +} + +type cinderVolumeCreater struct { + *cinderVolume + options volume.VolumeOptions +} + +var _ volume.Creater = &cinderVolumeCreater{} + +func (c *cinderVolumeCreater) Provision(pv *api.PersistentVolume) error { + volumeID, size, err := c.manager.CreateVolume(c) + if err != nil { + return err + } + pv.Spec.PersistentVolumeSource.Cinder.VolumeID = volumeID + pv.Spec.Capacity = api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse(fmt.Sprintf("%d", size)), + } + return nil +} + +func (c *cinderVolumeCreater) NewPersistentVolumeTemplate() (*api.PersistentVolume, error) { + // Provide dummy api.PersistentVolume.Spec, it will be filled in + // cinderVolumeCreater.Provision() + return &api.PersistentVolume{ + ObjectMeta: api.ObjectMeta{ + GenerateName: "pv-cinder-", + Labels: map[string]string{ + "createdby": "cinder_dynamic_provisioner", + }, + Annotations: map[string]string{}, + }, + Spec: api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: c.options.PersistentVolumeReclaimPolicy, + AccessModes: c.options.AccessModes, + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): c.options.Capacity, + }, + PersistentVolumeSource: api.PersistentVolumeSource{ + Cinder: &api.CinderVolumeSource{ + VolumeID: "dummy", + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + }, nil + +} diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/cinder/cinder_test.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/cinder/cinder_test.go index 5cf6dcec0bf4..605728471f37 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/cinder/cinder_test.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/cinder/cinder_test.go @@ -17,10 +17,12 @@ limitations under the License. package cinder import ( + "fmt" "os" "testing" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/volume" @@ -66,6 +68,17 @@ func (fake *fakePDManager) DetachDisk(c *cinderVolumeCleaner) error { return nil } +func (fake *fakePDManager) CreateVolume(c *cinderVolumeCreater) (string, int, error) { + return "test-volume-name", 1024 * 1024 * 1024, nil +} + +func (fake *fakePDManager) DeleteVolume(cd *cinderVolumeDeleter) error { + if cd.pdName != "test-volume-name" { + return fmt.Errorf("Deleter got unexpected volume name: %s", cd.pdName) + } + return nil +} + func TestPlugin(t *testing.T) { plugMgr := volume.VolumePluginMgr{} plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost("/tmp/fake", nil, nil)) @@ -130,4 +143,45 @@ func TestPlugin(t *testing.T) { } else if !os.IsNotExist(err) { t.Errorf("SetUp() failed: %v", err) } + + // Test Creater + cap := resource.MustParse("100Mi") + options := volume.VolumeOptions{ + Capacity: cap, + AccessModes: []api.PersistentVolumeAccessMode{ + api.ReadWriteOnce, + }, + PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimDelete, + } + creater, err := plug.(*cinderPlugin).newCreaterInternal(options, &fakePDManager{}) + persistentSpec, err := creater.NewPersistentVolumeTemplate() + if err != nil { + t.Errorf("NewPersistentVolumeTemplate() failed: %v", err) + } + + // get 2nd Creater - persistent volume controller will do the same + provisioner, err := plug.(*cinderPlugin).newCreaterInternal(options, &fakePDManager{}) + err = provisioner.Provision(persistentSpec) + if err != nil { + t.Errorf("Provision() failed: %v", err) + } + + if persistentSpec.Spec.PersistentVolumeSource.Cinder.VolumeID != "test-volume-name" { + t.Errorf("Provision() returned unexpected volume ID: %s", persistentSpec.Spec.PersistentVolumeSource.Cinder.VolumeID) + } + cap = persistentSpec.Spec.Capacity[api.ResourceStorage] + size := cap.Value() + if size != 1024*1024*1024 { + t.Errorf("Provision() returned unexpected volume size: %v", size) + } + + // Test Deleter + volSpec := &volume.Spec{ + PersistentVolume: persistentSpec, + } + deleter, err := plug.(*cinderPlugin).newDeleterInternal(volSpec, &fakePDManager{}) + err = deleter.Delete() + if err != nil { + t.Errorf("Deleter() failed: %v", err) + } } diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/cinder/cinder_util.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/cinder/cinder_util.go index 685333a436eb..baaab7bb6e72 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/cinder/cinder_util.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/cinder/cinder_util.go @@ -132,6 +132,40 @@ func (util *CinderDiskUtil) DetachDisk(cd *cinderVolumeCleaner) error { return nil } +func (util *CinderDiskUtil) DeleteVolume(cd *cinderVolumeDeleter) error { + cloud := cd.plugin.host.GetCloudProvider() + if cloud == nil { + glog.Errorf("Cloud provider not initialized properly") + return errors.New("Cloud provider not initialized properly") + } + + if err := cloud.(*openstack.OpenStack).DeleteVolume(cd.pdName); err != nil { + glog.V(2).Infof("Error deleting cinder volume %s: %v", cd.pdName, err) + return err + } + glog.V(2).Infof("Successfully deleted cinder volume %s", cd.pdName) + return nil +} + +func (util *CinderDiskUtil) CreateVolume(c *cinderVolumeCreater) (string, int, error) { + cloud := c.plugin.host.GetCloudProvider() + if cloud == nil { + glog.Errorf("Cloud provider not initialized properly") + return "", 0, errors.New("Cloud provider not initialized properly") + } + + // convert to GB with rounding + const giga = 1024 * 1024 * 1024 + volSize := int((c.options.Capacity.Value() + giga - 1) / giga) + name, err := cloud.(*openstack.OpenStack).CreateVolume(volSize) + if err != nil { + glog.V(2).Infof("Error creating cinder volume: %v", err) + return "", 0, err + } + glog.V(2).Infof("Successfully created cinder volume %s", name) + return name, volSize * giga, nil +} + type cinderSafeFormatAndMount struct { mount.Interface runner exec.Interface diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/gce_pd/gce_pd.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/gce_pd/gce_pd.go index e48ea049825c..b9cb09aa5707 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/gce_pd/gce_pd.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/gce_pd/gce_pd.go @@ -17,12 +17,14 @@ limitations under the License. package gce_pd import ( + "fmt" "os" "path" "strconv" "github.com/golang/glog" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util/exec" @@ -41,6 +43,8 @@ type gcePersistentDiskPlugin struct { var _ volume.VolumePlugin = &gcePersistentDiskPlugin{} var _ volume.PersistentVolumePlugin = &gcePersistentDiskPlugin{} +var _ volume.DeletableVolumePlugin = &gcePersistentDiskPlugin{} +var _ volume.ProvisionableVolumePlugin = &gcePersistentDiskPlugin{} const ( gcePersistentDiskPluginName = "kubernetes.io/gce-pd" @@ -122,12 +126,50 @@ func (plugin *gcePersistentDiskPlugin) newCleanerInternal(volName string, podUID }}, nil } +func (plugin *gcePersistentDiskPlugin) NewDeleter(spec *volume.Spec) (volume.Deleter, error) { + return plugin.newDeleterInternal(spec, &GCEDiskUtil{}) +} + +func (plugin *gcePersistentDiskPlugin) newDeleterInternal(spec *volume.Spec, manager pdManager) (volume.Deleter, error) { + if spec.PersistentVolume != nil && spec.PersistentVolume.Spec.GCEPersistentDisk == nil { + return nil, fmt.Errorf("spec.PersistentVolumeSource.GCEPersistentDisk is nil") + } + return &gcePersistentDiskDeleter{ + gcePersistentDisk: &gcePersistentDisk{ + volName: spec.Name(), + pdName: spec.PersistentVolume.Spec.GCEPersistentDisk.PDName, + manager: manager, + plugin: plugin, + }}, nil +} + +func (plugin *gcePersistentDiskPlugin) NewCreater(options volume.VolumeOptions) (volume.Creater, error) { + if len(options.AccessModes) == 0 { + options.AccessModes = plugin.GetAccessModes() + } + return plugin.newCreaterInternal(options, &GCEDiskUtil{}) +} + +func (plugin *gcePersistentDiskPlugin) newCreaterInternal(options volume.VolumeOptions, manager pdManager) (volume.Creater, error) { + return &gcePersistentDiskCreater{ + gcePersistentDisk: &gcePersistentDisk{ + manager: manager, + plugin: plugin, + }, + options: options, + }, nil +} + // Abstract interface to PD operations. type pdManager interface { // Attaches the disk to the kubelet's host machine. AttachAndMountDisk(b *gcePersistentDiskBuilder, globalPDPath string) error // Detaches the disk from the kubelet's host machine. DetachDisk(c *gcePersistentDiskCleaner) error + // Creates a volume + CreateVolume(creater *gcePersistentDiskCreater) (string, int, error) + // Deletes a volume + DeleteVolume(deleter *gcePersistentDiskDeleter) error } // gcePersistentDisk volumes are disk resources provided by Google Compute Engine @@ -295,3 +337,66 @@ func (c *gcePersistentDiskCleaner) TearDownAt(dir string) error { } return nil } + +type gcePersistentDiskDeleter struct { + *gcePersistentDisk +} + +var _ volume.Deleter = &gcePersistentDiskDeleter{} + +func (d *gcePersistentDiskDeleter) GetPath() string { + name := gcePersistentDiskPluginName + return d.plugin.host.GetPodVolumeDir(d.podUID, util.EscapeQualifiedNameForDisk(name), d.volName) +} + +func (d *gcePersistentDiskDeleter) Delete() error { + return d.manager.DeleteVolume(d) +} + +type gcePersistentDiskCreater struct { + *gcePersistentDisk + options volume.VolumeOptions +} + +var _ volume.Creater = &gcePersistentDiskCreater{} + +func (c *gcePersistentDiskCreater) Provision(pv *api.PersistentVolume) error { + volumeID, size, err := c.manager.CreateVolume(c) + if err != nil { + return err + } + pv.Spec.PersistentVolumeSource.GCEPersistentDisk.PDName = volumeID + pv.Spec.Capacity = api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse(fmt.Sprintf("%d", size)), + } + return nil +} + +func (c *gcePersistentDiskCreater) NewPersistentVolumeTemplate() (*api.PersistentVolume, error) { + // Provide dummy api.PersistentVolume.Spec, it will be filled in + // gcePersistentDiskCreater.Provision() + return &api.PersistentVolume{ + ObjectMeta: api.ObjectMeta{ + GenerateName: "pv-gce-", + Labels: map[string]string{ + "createdby": "gce_pd_dynamic_provisioner", + }, + Annotations: map[string]string{}, + }, + Spec: api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: c.options.PersistentVolumeReclaimPolicy, + AccessModes: c.options.AccessModes, + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): c.options.Capacity, + }, + PersistentVolumeSource: api.PersistentVolumeSource{ + GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{ + PDName: "dummy", + FSType: "ext4", + Partition: 0, + ReadOnly: false, + }, + }, + }, + }, nil +} diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/gce_pd/gce_pd_test.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/gce_pd/gce_pd_test.go index 2ced180a36db..3571e47fe67a 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/gce_pd/gce_pd_test.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/gce_pd/gce_pd_test.go @@ -17,10 +17,12 @@ limitations under the License. package gce_pd import ( + "fmt" "os" "testing" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/api/latest" "k8s.io/kubernetes/pkg/client/unversioned/testclient" "k8s.io/kubernetes/pkg/types" @@ -99,6 +101,17 @@ func (fake *fakePDManager) DetachDisk(c *gcePersistentDiskCleaner) error { return nil } +func (fake *fakePDManager) CreateVolume(c *gcePersistentDiskCreater) (string, int, error) { + return "test-gce-volume-name", 100 * 1024 * 1024, nil +} + +func (fake *fakePDManager) DeleteVolume(cd *gcePersistentDiskDeleter) error { + if cd.pdName != "test-gce-volume-name" { + return fmt.Errorf("Deleter got unexpected volume name: %s", cd.pdName) + } + return nil +} + func TestPlugin(t *testing.T) { plugMgr := volume.VolumePluginMgr{} plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost("/tmp/fake", nil, nil)) @@ -172,6 +185,47 @@ func TestPlugin(t *testing.T) { if !fakeManager.detachCalled { t.Errorf("Detach watch not called") } + + // Test Creater + cap := resource.MustParse("100Mi") + options := volume.VolumeOptions{ + Capacity: cap, + AccessModes: []api.PersistentVolumeAccessMode{ + api.ReadWriteOnce, + }, + PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimDelete, + } + creater, err := plug.(*gcePersistentDiskPlugin).newCreaterInternal(options, &fakePDManager{}) + persistentSpec, err := creater.NewPersistentVolumeTemplate() + if err != nil { + t.Errorf("NewPersistentVolumeTemplate() failed: %v", err) + } + + // get 2nd Creater - persistent volume controller will do the same + provisioner, err := plug.(*gcePersistentDiskPlugin).newCreaterInternal(options, &fakePDManager{}) + err = provisioner.Provision(persistentSpec) + if err != nil { + t.Errorf("Provision() failed: %v", err) + } + + if persistentSpec.Spec.PersistentVolumeSource.GCEPersistentDisk.PDName != "test-gce-volume-name" { + t.Errorf("Provision() returned unexpected volume ID: %s", persistentSpec.Spec.PersistentVolumeSource.GCEPersistentDisk.PDName) + } + cap = persistentSpec.Spec.Capacity[api.ResourceStorage] + size := cap.Value() + if size != 100*1024*1024 { + t.Errorf("Provision() returned unexpected volume size: %v", size) + } + + // Test Deleter + volSpec := &volume.Spec{ + PersistentVolume: persistentSpec, + } + deleter, err := plug.(*gcePersistentDiskPlugin).newDeleterInternal(volSpec, &fakePDManager{}) + err = deleter.Delete() + if err != nil { + t.Errorf("Deleter() failed: %v", err) + } } func TestPersistentClaimReadOnlyFlag(t *testing.T) { diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/gce_pd/gce_util.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/gce_pd/gce_util.go index 47ddeb5f518d..1555a44ba6b3 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/gce_pd/gce_util.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/volume/gce_pd/gce_util.go @@ -17,6 +17,7 @@ limitations under the License. package gce_pd import ( + "errors" "fmt" "os" "path" @@ -120,6 +121,41 @@ func (util *GCEDiskUtil) DetachDisk(c *gcePersistentDiskCleaner) error { return nil } +func (util *GCEDiskUtil) DeleteVolume(d *gcePersistentDiskDeleter) error { + cloud := d.plugin.host.GetCloudProvider() + if cloud == nil { + glog.Errorf("Cloud provider not initialized properly") + return errors.New("Cloud provider not initialized properly") + } + + if err := cloud.(*gce_cloud.GCECloud).DeleteDisk(d.pdName); err != nil { + glog.V(2).Infof("Error deleting GCE PD volume %s: %v", d.pdName, err) + return err + } + glog.V(2).Infof("Successfully deleted GCE PD volume %s", d.pdName) + return nil +} + +func (gceutil *GCEDiskUtil) CreateVolume(c *gcePersistentDiskCreater) (string, int, error) { + cloud := c.plugin.host.GetCloudProvider() + if cloud == nil { + glog.Errorf("Cloud provider not initialized properly") + return "", 0, errors.New("Cloud provider not initialized properly") + } + + name := fmt.Sprintf("kube-dynamic-%s", util.NewUUID()) + // convert to GB with rounding + const giga = 1024 * 1024 * 1024 + volSize := int((c.options.Capacity.Value() + giga - 1) / giga) + err := cloud.(*gce_cloud.GCECloud).CreateDisk(name, int64(volSize)) + if err != nil { + glog.V(2).Infof("Error creating GCE PD volume: %v", err) + return "", 0, err + } + glog.V(2).Infof("Successfully created GCE PD volume %s", name) + return name, volSize * giga, nil +} + // Attaches the specified persistent disk device to node, verifies that it is attached, and retries if it fails. func attachDiskAndVerify(b *gcePersistentDiskBuilder, sdBeforeSet sets.String) (string, error) { devicePaths := getDiskByIdPaths(b.gcePersistentDisk)