Commit 4fc1bf1f authored by markturansky's avatar markturansky

Added PersistentVolumeController

parent d3243b87
...@@ -137,6 +137,7 @@ func NewCMServer() *CMServer { ...@@ -137,6 +137,7 @@ func NewCMServer() *CMServer {
PersistentVolumeRecyclerIncrementTimeoutNFS: 30, PersistentVolumeRecyclerIncrementTimeoutNFS: 30,
PersistentVolumeRecyclerMinimumTimeoutHostPath: 60, PersistentVolumeRecyclerMinimumTimeoutHostPath: 60,
PersistentVolumeRecyclerIncrementTimeoutHostPath: 30, PersistentVolumeRecyclerIncrementTimeoutHostPath: 30,
EnableHostPathProvisioning: false,
}, },
KubeAPIQPS: 20.0, KubeAPIQPS: 20.0,
KubeAPIBurst: 30, KubeAPIBurst: 30,
...@@ -176,6 +177,7 @@ type VolumeConfigFlags struct { ...@@ -176,6 +177,7 @@ type VolumeConfigFlags struct {
PersistentVolumeRecyclerPodTemplateFilePathHostPath string PersistentVolumeRecyclerPodTemplateFilePathHostPath string
PersistentVolumeRecyclerMinimumTimeoutHostPath int PersistentVolumeRecyclerMinimumTimeoutHostPath int
PersistentVolumeRecyclerIncrementTimeoutHostPath int PersistentVolumeRecyclerIncrementTimeoutHostPath int
EnableHostPathProvisioning bool
} }
// AddFlags adds flags for a specific CMServer to the specified FlagSet // AddFlags adds flags for a specific CMServer to the specified FlagSet
...@@ -201,6 +203,7 @@ func (s *CMServer) AddFlags(fs *pflag.FlagSet) { ...@@ -201,6 +203,7 @@ func (s *CMServer) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&s.VolumeConfigFlags.PersistentVolumeRecyclerPodTemplateFilePathHostPath, "pv-recycler-pod-template-filepath-hostpath", s.VolumeConfigFlags.PersistentVolumeRecyclerPodTemplateFilePathHostPath, "The file path to a pod definition used as a template for HostPath persistent volume recycling. This is for development and testing only and will not work in a multi-node cluster.") fs.StringVar(&s.VolumeConfigFlags.PersistentVolumeRecyclerPodTemplateFilePathHostPath, "pv-recycler-pod-template-filepath-hostpath", s.VolumeConfigFlags.PersistentVolumeRecyclerPodTemplateFilePathHostPath, "The file path to a pod definition used as a template for HostPath persistent volume recycling. This is for development and testing only and will not work in a multi-node cluster.")
fs.IntVar(&s.VolumeConfigFlags.PersistentVolumeRecyclerMinimumTimeoutHostPath, "pv-recycler-minimum-timeout-hostpath", s.VolumeConfigFlags.PersistentVolumeRecyclerMinimumTimeoutHostPath, "The minimum ActiveDeadlineSeconds to use for a HostPath Recycler pod. This is for development and testing only and will not work in a multi-node cluster.") fs.IntVar(&s.VolumeConfigFlags.PersistentVolumeRecyclerMinimumTimeoutHostPath, "pv-recycler-minimum-timeout-hostpath", s.VolumeConfigFlags.PersistentVolumeRecyclerMinimumTimeoutHostPath, "The minimum ActiveDeadlineSeconds to use for a HostPath Recycler pod. This is for development and testing only and will not work in a multi-node cluster.")
fs.IntVar(&s.VolumeConfigFlags.PersistentVolumeRecyclerIncrementTimeoutHostPath, "pv-recycler-timeout-increment-hostpath", s.VolumeConfigFlags.PersistentVolumeRecyclerIncrementTimeoutHostPath, "the increment of time added per Gi to ActiveDeadlineSeconds for a HostPath scrubber pod. This is for development and testing only and will not work in a multi-node cluster.") fs.IntVar(&s.VolumeConfigFlags.PersistentVolumeRecyclerIncrementTimeoutHostPath, "pv-recycler-timeout-increment-hostpath", s.VolumeConfigFlags.PersistentVolumeRecyclerIncrementTimeoutHostPath, "the increment of time added per Gi to ActiveDeadlineSeconds for a HostPath scrubber pod. This is for development and testing only and will not work in a multi-node cluster.")
fs.BoolVar(&s.VolumeConfigFlags.EnableHostPathProvisioning, "enable-hostpath-provisioner", s.VolumeConfigFlags.EnableHostPathProvisioning, "Enable HostPath PV provisioning when running without a cloud provider. This allows testing and development of provisioning features. HostPath provisioning is not supported in any way, won't work in a multi-node cluster, and should not be used for anything other than testing or development.")
fs.IntVar(&s.TerminatedPodGCThreshold, "terminated-pod-gc-threshold", s.TerminatedPodGCThreshold, "Number of terminated pods that can exist before the terminated pod garbage collector starts deleting terminated pods. If <= 0, the terminated pod garbage collector is disabled.") fs.IntVar(&s.TerminatedPodGCThreshold, "terminated-pod-gc-threshold", s.TerminatedPodGCThreshold, "Number of terminated pods that can exist before the terminated pod garbage collector starts deleting terminated pods. If <= 0, the terminated pod garbage collector is disabled.")
fs.DurationVar(&s.HorizontalPodAutoscalerSyncPeriod, "horizontal-pod-autoscaler-sync-period", s.HorizontalPodAutoscalerSyncPeriod, "The period for syncing the number of pods in horizontal pod autoscaler.") fs.DurationVar(&s.HorizontalPodAutoscalerSyncPeriod, "horizontal-pod-autoscaler-sync-period", s.HorizontalPodAutoscalerSyncPeriod, "The period for syncing the number of pods in horizontal pod autoscaler.")
fs.DurationVar(&s.DeploymentControllerSyncPeriod, "deployment-controller-sync-period", s.DeploymentControllerSyncPeriod, "Period for syncing the deployments.") fs.DurationVar(&s.DeploymentControllerSyncPeriod, "deployment-controller-sync-period", s.DeploymentControllerSyncPeriod, "Period for syncing the deployments.")
...@@ -385,15 +388,29 @@ func (s *CMServer) Run(_ []string) error { ...@@ -385,15 +388,29 @@ func (s *CMServer) Run(_ []string) error {
} }
} }
volumePlugins := ProbeRecyclableVolumePlugins(s.VolumeConfigFlags)
provisioner, err := NewVolumeProvisioner(cloud, s.VolumeConfigFlags)
if err != nil {
glog.Fatal("A Provisioner could not be created, but one was expected. Provisioning will not work. This functionality is considered an early Alpha version.")
}
pvclaimBinder := persistentvolumecontroller.NewPersistentVolumeClaimBinder(clientForUserAgentOrDie(*kubeconfig, "persistent-volume-binder"), s.PVClaimBinderSyncPeriod) pvclaimBinder := persistentvolumecontroller.NewPersistentVolumeClaimBinder(clientForUserAgentOrDie(*kubeconfig, "persistent-volume-binder"), s.PVClaimBinderSyncPeriod)
pvclaimBinder.Run() pvclaimBinder.Run()
pvRecycler, err := persistentvolumecontroller.NewPersistentVolumeRecycler(clientForUserAgentOrDie(*kubeconfig, "persistent-volume-recycler"), s.PVClaimBinderSyncPeriod, ProbeRecyclableVolumePlugins(s.VolumeConfigFlags)) pvRecycler, err := persistentvolumecontroller.NewPersistentVolumeRecycler(clientForUserAgentOrDie(*kubeconfig, "persistent-volume-recycler"), s.PVClaimBinderSyncPeriod, ProbeRecyclableVolumePlugins(s.VolumeConfigFlags), cloud)
if err != nil { if err != nil {
glog.Fatalf("Failed to start persistent volume recycler: %+v", err) glog.Fatalf("Failed to start persistent volume recycler: %+v", err)
} }
pvRecycler.Run() pvRecycler.Run()
if provisioner != nil {
pvController, err := persistentvolumecontroller.NewPersistentVolumeProvisionerController(persistentvolumecontroller.NewControllerClient(clientForUserAgentOrDie(*kubeconfig, "persistent-volume-provisioner")), s.PVClaimBinderSyncPeriod, volumePlugins, provisioner, cloud)
if err != nil {
glog.Fatalf("Failed to start persistent volume provisioner controller: %+v", err)
}
pvController.Run()
}
var rootCA []byte var rootCA []byte
if s.RootCAFile != "" { if s.RootCAFile != "" {
......
...@@ -21,12 +21,18 @@ import ( ...@@ -21,12 +21,18 @@ import (
// This should probably be part of some configuration fed into the build for a // This should probably be part of some configuration fed into the build for a
// given binary target. // given binary target.
"fmt"
//Cloud providers //Cloud providers
_ "k8s.io/kubernetes/pkg/cloudprovider/providers" _ "k8s.io/kubernetes/pkg/cloudprovider/providers"
// Volume plugins // Volume plugins
"k8s.io/kubernetes/pkg/cloudprovider"
"k8s.io/kubernetes/pkg/util/io" "k8s.io/kubernetes/pkg/util/io"
"k8s.io/kubernetes/pkg/volume" "k8s.io/kubernetes/pkg/volume"
"k8s.io/kubernetes/pkg/volume/aws_ebs"
"k8s.io/kubernetes/pkg/volume/cinder"
"k8s.io/kubernetes/pkg/volume/gce_pd"
"k8s.io/kubernetes/pkg/volume/host_path" "k8s.io/kubernetes/pkg/volume/host_path"
"k8s.io/kubernetes/pkg/volume/nfs" "k8s.io/kubernetes/pkg/volume/nfs"
...@@ -51,7 +57,7 @@ func ProbeRecyclableVolumePlugins(flags VolumeConfigFlags) []volume.VolumePlugin ...@@ -51,7 +57,7 @@ func ProbeRecyclableVolumePlugins(flags VolumeConfigFlags) []volume.VolumePlugin
RecyclerTimeoutIncrement: flags.PersistentVolumeRecyclerIncrementTimeoutHostPath, RecyclerTimeoutIncrement: flags.PersistentVolumeRecyclerIncrementTimeoutHostPath,
RecyclerPodTemplate: volume.NewPersistentVolumeRecyclerPodTemplate(), RecyclerPodTemplate: volume.NewPersistentVolumeRecyclerPodTemplate(),
} }
if err := attemptToLoadRecycler(flags.PersistentVolumeRecyclerPodTemplateFilePathHostPath, &hostPathConfig); err != nil { if err := AttemptToLoadRecycler(flags.PersistentVolumeRecyclerPodTemplateFilePathHostPath, &hostPathConfig); err != nil {
glog.Fatalf("Could not create hostpath recycler pod from file %s: %+v", flags.PersistentVolumeRecyclerPodTemplateFilePathHostPath, err) glog.Fatalf("Could not create hostpath recycler pod from file %s: %+v", flags.PersistentVolumeRecyclerPodTemplateFilePathHostPath, err)
} }
allPlugins = append(allPlugins, host_path.ProbeVolumePlugins(hostPathConfig)...) allPlugins = append(allPlugins, host_path.ProbeVolumePlugins(hostPathConfig)...)
...@@ -61,18 +67,49 @@ func ProbeRecyclableVolumePlugins(flags VolumeConfigFlags) []volume.VolumePlugin ...@@ -61,18 +67,49 @@ func ProbeRecyclableVolumePlugins(flags VolumeConfigFlags) []volume.VolumePlugin
RecyclerTimeoutIncrement: flags.PersistentVolumeRecyclerIncrementTimeoutNFS, RecyclerTimeoutIncrement: flags.PersistentVolumeRecyclerIncrementTimeoutNFS,
RecyclerPodTemplate: volume.NewPersistentVolumeRecyclerPodTemplate(), RecyclerPodTemplate: volume.NewPersistentVolumeRecyclerPodTemplate(),
} }
if err := attemptToLoadRecycler(flags.PersistentVolumeRecyclerPodTemplateFilePathNFS, &nfsConfig); err != nil { if err := AttemptToLoadRecycler(flags.PersistentVolumeRecyclerPodTemplateFilePathNFS, &nfsConfig); err != nil {
glog.Fatalf("Could not create NFS recycler pod from file %s: %+v", flags.PersistentVolumeRecyclerPodTemplateFilePathNFS, err) glog.Fatalf("Could not create NFS recycler pod from file %s: %+v", flags.PersistentVolumeRecyclerPodTemplateFilePathNFS, err)
} }
allPlugins = append(allPlugins, nfs.ProbeVolumePlugins(nfsConfig)...) allPlugins = append(allPlugins, nfs.ProbeVolumePlugins(nfsConfig)...)
allPlugins = append(allPlugins, aws_ebs.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, gce_pd.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, cinder.ProbeVolumePlugins()...)
return allPlugins return allPlugins
} }
// attemptToLoadRecycler tries decoding a pod from a filepath for use as a recycler for a volume. // NewVolumeProvisioner returns a volume provisioner to use when running in a cloud or development environment.
// The beta implementation of provisioning allows 1 implied provisioner per cloud, until we allow configuration of many.
// We explicitly map clouds to volume plugins here which allows us to configure many later without backwards compatibility issues.
// Not all cloudproviders have provisioning capability, which is the reason for the bool in the return to tell the caller to expect one or not.
func NewVolumeProvisioner(cloud cloudprovider.Interface, flags VolumeConfigFlags) (volume.ProvisionableVolumePlugin, error) {
switch {
case cloud == nil && flags.EnableHostPathProvisioning:
return getProvisionablePluginFromVolumePlugins(host_path.ProbeVolumePlugins(volume.VolumeConfig{}))
// case cloud != nil && aws.ProviderName == cloud.ProviderName():
// return getProvisionablePluginFromVolumePlugins(aws_ebs.ProbeVolumePlugins())
// case cloud != nil && gce.ProviderName == cloud.ProviderName():
// return getProvisionablePluginFromVolumePlugins(gce_pd.ProbeVolumePlugins())
// case cloud != nil && openstack.ProviderName == cloud.ProviderName():
// return getProvisionablePluginFromVolumePlugins(cinder.ProbeVolumePlugins())
}
return nil, nil
}
func getProvisionablePluginFromVolumePlugins(plugins []volume.VolumePlugin) (volume.ProvisionableVolumePlugin, error) {
for _, plugin := range plugins {
if provisonablePlugin, ok := plugin.(volume.ProvisionableVolumePlugin); ok {
return provisonablePlugin, nil
}
}
return nil, fmt.Errorf("ProvisionablePlugin expected but not found in %#v: ", plugins)
}
// AttemptToLoadRecycler tries decoding a pod from a filepath for use as a recycler for a volume.
// If successful, this method will set the recycler on the config. // If successful, this method will set the recycler on the config.
// If unsucessful, an error is returned. // If unsuccessful, an error is returned. Function is exported for reuse downstream.
func attemptToLoadRecycler(path string, config *volume.VolumeConfig) error { func AttemptToLoadRecycler(path string, config *volume.VolumeConfig) error {
if path != "" { if path != "" {
recyclerPod, err := io.LoadPodFromFile(path) recyclerPod, err := io.LoadPodFromFile(path)
if err != nil { if err != nil {
......
...@@ -25,7 +25,7 @@ import ( ...@@ -25,7 +25,7 @@ import (
"strconv" "strconv"
"time" "time"
"k8s.io/kubernetes/cmd/kube-controller-manager/app" kubecontrollermanager "k8s.io/kubernetes/cmd/kube-controller-manager/app"
"k8s.io/kubernetes/contrib/mesos/pkg/node" "k8s.io/kubernetes/contrib/mesos/pkg/node"
"k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/unversioned"
client "k8s.io/kubernetes/pkg/client/unversioned" client "k8s.io/kubernetes/pkg/client/unversioned"
...@@ -57,14 +57,14 @@ import ( ...@@ -57,14 +57,14 @@ import (
// CMServer is the main context object for the controller manager. // CMServer is the main context object for the controller manager.
type CMServer struct { type CMServer struct {
*app.CMServer *kubecontrollermanager.CMServer
UseHostPortEndpoints bool UseHostPortEndpoints bool
} }
// NewCMServer creates a new CMServer with a default config. // NewCMServer creates a new CMServer with a default config.
func NewCMServer() *CMServer { func NewCMServer() *CMServer {
s := &CMServer{ s := &CMServer{
CMServer: app.NewCMServer(), CMServer: kubecontrollermanager.NewCMServer(),
} }
s.CloudProvider = mesos.ProviderName s.CloudProvider = mesos.ProviderName
s.UseHostPortEndpoints = true s.UseHostPortEndpoints = true
...@@ -167,14 +167,29 @@ func (s *CMServer) Run(_ []string) error { ...@@ -167,14 +167,29 @@ func (s *CMServer) Run(_ []string) error {
namespaceController := namespacecontroller.NewNamespaceController(kubeClient, &unversioned.APIVersions{}, s.NamespaceSyncPeriod) namespaceController := namespacecontroller.NewNamespaceController(kubeClient, &unversioned.APIVersions{}, s.NamespaceSyncPeriod)
namespaceController.Run() namespaceController.Run()
volumePlugins := kubecontrollermanager.ProbeRecyclableVolumePlugins(s.VolumeConfigFlags)
provisioner, err := kubecontrollermanager.NewVolumeProvisioner(cloud, s.VolumeConfigFlags)
if err != nil {
glog.Fatal("A Provisioner could not be created, but one was expected. Provisioning will not work. This functionality is considered an early Alpha version.")
}
pvclaimBinder := persistentvolumecontroller.NewPersistentVolumeClaimBinder(kubeClient, s.PVClaimBinderSyncPeriod) pvclaimBinder := persistentvolumecontroller.NewPersistentVolumeClaimBinder(kubeClient, s.PVClaimBinderSyncPeriod)
pvclaimBinder.Run() pvclaimBinder.Run()
pvRecycler, err := persistentvolumecontroller.NewPersistentVolumeRecycler(kubeClient, s.PVClaimBinderSyncPeriod, app.ProbeRecyclableVolumePlugins(s.VolumeConfigFlags))
pvRecycler, err := persistentvolumecontroller.NewPersistentVolumeRecycler(kubeClient, s.PVClaimBinderSyncPeriod, kubecontrollermanager.ProbeRecyclableVolumePlugins(s.VolumeConfigFlags), cloud)
if err != nil { if err != nil {
glog.Fatalf("Failed to start persistent volume recycler: %+v", err) glog.Fatalf("Failed to start persistent volume recycler: %+v", err)
} }
pvRecycler.Run() pvRecycler.Run()
if provisioner != nil {
pvController, err := persistentvolumecontroller.NewPersistentVolumeProvisionerController(persistentvolumecontroller.NewControllerClient(kubeClient), s.PVClaimBinderSyncPeriod, volumePlugins, provisioner, cloud)
if err != nil {
glog.Fatalf("Failed to start persistent volume provisioner controller: %+v", err)
}
pvController.Run()
}
var rootCA []byte var rootCA []byte
if s.RootCAFile != "" { if s.RootCAFile != "" {
......
...@@ -66,6 +66,7 @@ kube-controller-manager ...@@ -66,6 +66,7 @@ kube-controller-manager
--deleting-pods-burst=10: Number of nodes on which pods are bursty deleted in case of node failure. For more details look into RateLimiter. --deleting-pods-burst=10: Number of nodes on which pods are bursty deleted in case of node failure. For more details look into RateLimiter.
--deleting-pods-qps=0.1: Number of nodes per second on which pods are deleted in case of node failure. --deleting-pods-qps=0.1: Number of nodes per second on which pods are deleted in case of node failure.
--deployment-controller-sync-period=30s: Period for syncing the deployments. --deployment-controller-sync-period=30s: Period for syncing the deployments.
--enable-hostpath-provisioner[=false]: Enable HostPath PV provisioning when running without a cloud provider. This allows testing and development of provisioning features. HostPath provisioning is not supported in any way, won't work in a multi-node cluster, and should not be used for anything other than testing or development.
--google-json-key="": The Google Cloud Platform Service Account JSON Key to use for authentication. --google-json-key="": The Google Cloud Platform Service Account JSON Key to use for authentication.
--horizontal-pod-autoscaler-sync-period=30s: The period for syncing the number of pods in horizontal pod autoscaler. --horizontal-pod-autoscaler-sync-period=30s: The period for syncing the number of pods in horizontal pod autoscaler.
--kube-api-burst=30: Burst to use while talking with kubernetes apiserver --kube-api-burst=30: Burst to use while talking with kubernetes apiserver
...@@ -96,7 +97,7 @@ kube-controller-manager ...@@ -96,7 +97,7 @@ kube-controller-manager
--terminated-pod-gc-threshold=12500: Number of terminated pods that can exist before the terminated pod garbage collector starts deleting terminated pods. If <= 0, the terminated pod garbage collector is disabled. --terminated-pod-gc-threshold=12500: Number of terminated pods that can exist before the terminated pod garbage collector starts deleting terminated pods. If <= 0, the terminated pod garbage collector is disabled.
``` ```
###### Auto generated by spf13/cobra on 30-Nov-2015 ###### Auto generated by spf13/cobra on 9-Dec-2015
<!-- BEGIN MUNGE: GENERATED_ANALYTICS --> <!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
......
<!-- BEGIN MUNGE: UNVERSIONED_WARNING -->
<!-- BEGIN STRIP_FOR_RELEASE -->
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<h2>PLEASE NOTE: This document applies to the HEAD of the source tree</h2>
If you are using a released version of Kubernetes, you should
refer to the docs that go with that version.
<strong>
The latest release of this document can be found
[here](http://releases.k8s.io/release-1.1/examples/experimental/persistent-volume-provisioning/README.md).
Documentation for other releases can be found at
[releases.k8s.io](http://releases.k8s.io).
</strong>
--
<!-- END STRIP_FOR_RELEASE -->
<!-- END MUNGE: UNVERSIONED_WARNING -->
## Persistent Volume Provisioning
This example shows how to use experimental persistent volume provisioning.
### Pre-requisites
This example assumes that you have an understanding of Kubernetes administration and can modify the
scripts that launch kube-controller-manager.
### Admin Configuration
No configuration is required by the admin! 3 cloud providers will be provided in the alpha version
of this feature: EBS, GCE, and Cinder.
When Kubernetes is running in one of those clouds, there will be an implied provisioner.
There is no provisioner when running outside of any of those 3 cloud providers.
A fourth provisioner is included for testing and development only. It creates HostPath volumes,
which will never work outside of a single node cluster. It is not supported in any way except for
local for testing and development.
### User provisioning requests
Users request dynamically provisioned storage by including a storage class in their `PersistentVolumeClaim`.
The annotation `volume.alpha.kubernetes.io/storage-class` is used to access this experimental feature.
In the future, admins will be able to define many storage classes.
The storage class may remain in an annotation or become a field on the claim itself.
> The value of the storage-class annotation does not matter in the alpha version of this feature. There is
a single implied provisioner per cloud (which creates 1 kind of volume in the provider). The full version of the feature
will require that this value matches what is configured by the administrator.
```
{
"kind": "PersistentVolumeClaim",
"apiVersion": "v1",
"metadata": {
"name": "claim1",
"annotations": {
"volume.alpha.kubernetes.io/storage-class": "foo"
}
},
"spec": {
"accessModes": [
"ReadWriteOnce"
],
"resources": {
"requests": {
"storage": "3Gi"
}
}
}
}
```
### Sample output
This example uses HostPath but any provisioner would follow the same flow.
First we note there are no Persistent Volumes in the cluster. After creating a claim, we see a new PV is created
and automatically bound to the claim requesting storage.
```
$ kubectl get pv
$ kubectl create -f examples/experimental/persistent-volume-provisioning/claim1.json
I1012 13:07:57.666759 22875 decoder.go:141] decoding stream as JSON
persistentvolumeclaim "claim1" created
$ kubectl get pv
NAME LABELS CAPACITY ACCESSMODES STATUS CLAIM REASON AGE
pv-hostpath-r6z5o createdby=hostpath-dynamic-provisioner 3Gi RWO Bound default/claim1 2s
$ kubectl get pvc
NAME LABELS STATUS VOLUME CAPACITY ACCESSMODES AGE
claim1 <none> Bound pv-hostpath-r6z5o 3Gi RWO 7s
# delete the claim to release the volume
$ kubectl delete pvc claim1
persistentvolumeclaim "claim1" deleted
# the volume is deleted in response to being release of its claim
$ kubectl get pv
```
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/examples/experimental/persistent-volume-provisioning/README.md?pixel)]()
<!-- END MUNGE: GENERATED_ANALYTICS -->
{
"kind": "PersistentVolumeClaim",
"apiVersion": "v1",
"metadata": {
"name": "claim1",
"annotations": {
"volume.alpha.kubernetes.io/storage-class": "foo"
}
},
"spec": {
"accessModes": [
"ReadWriteOnce"
],
"resources": {
"requests": {
"storage": "3Gi"
}
}
}
}
{
"kind": "PersistentVolumeClaim",
"apiVersion": "v1",
"metadata": {
"name": "claim2",
"annotations": {
"volume.alpha.kubernetes.io/storage-class": "bar"
}
},
"spec": {
"accessModes": [
"ReadWriteOnce"
],
"resources": {
"requests": {
"storage": "3Gi"
}
}
}
}
...@@ -91,6 +91,7 @@ RKT_PATH=${RKT_PATH:-""} ...@@ -91,6 +91,7 @@ RKT_PATH=${RKT_PATH:-""}
RKT_STAGE1_IMAGE=${RKT_STAGE1_IMAGE:-""} RKT_STAGE1_IMAGE=${RKT_STAGE1_IMAGE:-""}
CHAOS_CHANCE=${CHAOS_CHANCE:-0.0} CHAOS_CHANCE=${CHAOS_CHANCE:-0.0}
CPU_CFS_QUOTA=${CPU_CFS_QUOTA:-false} CPU_CFS_QUOTA=${CPU_CFS_QUOTA:-false}
ENABLE_HOSTPATH_PROVISIONER=${ENABLE_HOSTPATH_PROVISIONER:-"false"}
function test_apiserver_off { function test_apiserver_off {
# For the common local scenario, fail fast if server is already running. # For the common local scenario, fail fast if server is already running.
...@@ -250,6 +251,7 @@ function start_controller_manager { ...@@ -250,6 +251,7 @@ function start_controller_manager {
--v=${LOG_LEVEL} \ --v=${LOG_LEVEL} \
--service-account-private-key-file="${SERVICE_ACCOUNT_KEY}" \ --service-account-private-key-file="${SERVICE_ACCOUNT_KEY}" \
--root-ca-file="${ROOT_CA_FILE}" \ --root-ca-file="${ROOT_CA_FILE}" \
--enable-hostpath-provisioner="${ENABLE_HOSTPATH_PROVISIONER}" \
--master="${API_HOST}:${API_PORT}" >"${CTLRMGR_LOG}" 2>&1 & --master="${API_HOST}:${API_PORT}" >"${CTLRMGR_LOG}" 2>&1 &
CTLRMGR_PID=$! CTLRMGR_PID=$!
} }
......
...@@ -78,6 +78,7 @@ e2e-output-dir ...@@ -78,6 +78,7 @@ e2e-output-dir
e2e-verify-service-account e2e-verify-service-account
enable-debugging-handlers enable-debugging-handlers
enable-server enable-server
enable-hostpath-provisioner
etcd-config etcd-config
etcd-prefix etcd-prefix
etcd-server etcd-server
......
...@@ -86,8 +86,7 @@ func NewPersistentVolumeClaimBinder(kubeClient client.Interface, syncPeriod time ...@@ -86,8 +86,7 @@ func NewPersistentVolumeClaimBinder(kubeClient client.Interface, syncPeriod time
framework.ResourceEventHandlerFuncs{ framework.ResourceEventHandlerFuncs{
AddFunc: binder.addClaim, AddFunc: binder.addClaim,
UpdateFunc: binder.updateClaim, UpdateFunc: binder.updateClaim,
// no DeleteFunc needed. a claim requires no clean-up. DeleteFunc: binder.deleteClaim,
// syncVolume handles the missing claim
}, },
) )
...@@ -145,6 +144,33 @@ func (binder *PersistentVolumeClaimBinder) updateClaim(oldObj, newObj interface{ ...@@ -145,6 +144,33 @@ func (binder *PersistentVolumeClaimBinder) updateClaim(oldObj, newObj interface{
} }
} }
func (binder *PersistentVolumeClaimBinder) deleteClaim(obj interface{}) {
binder.lock.Lock()
defer binder.lock.Unlock()
var volume *api.PersistentVolume
if pvc, ok := obj.(*api.PersistentVolumeClaim); ok {
if pvObj, exists, _ := binder.volumeIndex.GetByKey(pvc.Spec.VolumeName); exists {
if pv, ok := pvObj.(*api.PersistentVolume); ok {
volume = pv
}
}
}
if unk, ok := obj.(cache.DeletedFinalStateUnknown); ok && unk.Obj != nil {
if pv, ok := unk.Obj.(*api.PersistentVolume); ok {
volume = pv
}
}
// sync the volume when its claim is deleted. Explicitly sync'ing the volume here in response to
// claim deletion prevents the volume from waiting until the next sync period for its Release.
if volume != nil {
err := syncVolume(binder.volumeIndex, binder.client, volume)
if err != nil {
glog.Errorf("PVClaimBinder could not update volume %s from deleteClaim handler: %+v", volume.Name, err)
}
}
}
func syncVolume(volumeIndex *persistentVolumeOrderedIndex, binderClient binderClient, volume *api.PersistentVolume) (err error) { func syncVolume(volumeIndex *persistentVolumeOrderedIndex, binderClient binderClient, volume *api.PersistentVolume) (err error) {
glog.V(5).Infof("Synchronizing PersistentVolume[%s], current phase: %s\n", volume.Name, volume.Status.Phase) glog.V(5).Infof("Synchronizing PersistentVolume[%s], current phase: %s\n", volume.Name, volume.Status.Phase)
...@@ -166,6 +192,11 @@ func syncVolume(volumeIndex *persistentVolumeOrderedIndex, binderClient binderCl ...@@ -166,6 +192,11 @@ func syncVolume(volumeIndex *persistentVolumeOrderedIndex, binderClient binderCl
volumeIndex.Add(volume) volumeIndex.Add(volume)
} }
if isBeingProvisioned(volume) {
glog.V(4).Infof("Skipping PersistentVolume[%s], waiting for provisioning to finish", volume.Name)
return nil
}
switch currentPhase { switch currentPhase {
case api.VolumePending: case api.VolumePending:
...@@ -275,24 +306,31 @@ func syncClaim(volumeIndex *persistentVolumeOrderedIndex, binderClient binderCli ...@@ -275,24 +306,31 @@ func syncClaim(volumeIndex *persistentVolumeOrderedIndex, binderClient binderCli
switch claim.Status.Phase { switch claim.Status.Phase {
case api.ClaimPending: case api.ClaimPending:
// claims w/ a storage-class annotation for provisioning with *only* match volumes with a ClaimRef of the claim.
volume, err := volumeIndex.findBestMatchForClaim(claim) volume, err := volumeIndex.findBestMatchForClaim(claim)
if err != nil { if err != nil {
return err return err
} }
if volume == nil { if volume == nil {
glog.V(5).Infof("A volume match does not exist for persistent claim: %s", claim.Name) glog.V(5).Infof("A volume match does not exist for persistent claim: %s", claim.Name)
return nil return nil
} }
// create a reference to the claim and assign it to the volume being bound. if isBeingProvisioned(volume) {
// the volume is a pointer and assigning the reference fixes a race condition where another glog.V(5).Infof("PersistentVolume[%s] for PersistentVolumeClaim[%s/%s] is still being provisioned.", volume.Name, claim.Namespace, claim.Name)
// claim might match this volume but before the claimRef is persistent in the next case statement return nil
}
claimRef, err := api.GetReference(claim) claimRef, err := api.GetReference(claim)
if err != nil { if err != nil {
return fmt.Errorf("Unexpected error getting claim reference: %v\n", err) return fmt.Errorf("Unexpected error getting claim reference: %v\n", err)
} }
// make a binding reference to the claim and ensure to update the local index to prevent dupe bindings // Make a binding reference to the claim by persisting claimRef on the volume.
// The local cache must be updated with the new bind to prevent subsequent
// claims from binding to the volume.
if volume.Spec.ClaimRef == nil {
clone, err := conversion.NewCloner().DeepCopy(volume) clone, err := conversion.NewCloner().DeepCopy(volume)
if err != nil { if err != nil {
return fmt.Errorf("Error cloning pv: %v", err) return fmt.Errorf("Error cloning pv: %v", err)
...@@ -308,6 +346,7 @@ func syncClaim(volumeIndex *persistentVolumeOrderedIndex, binderClient binderCli ...@@ -308,6 +346,7 @@ func syncClaim(volumeIndex *persistentVolumeOrderedIndex, binderClient binderCli
volume = updatedVolume volume = updatedVolume
volumeIndex.Update(updatedVolume) volumeIndex.Update(updatedVolume)
} }
}
// the bind is persisted on the volume above and will always match the claim in a search. // the bind is persisted on the volume above and will always match the claim in a search.
// claim would remain Pending if the update fails, so processing this state is idempotent. // claim would remain Pending if the update fails, so processing this state is idempotent.
...@@ -341,6 +380,14 @@ func syncClaim(volumeIndex *persistentVolumeOrderedIndex, binderClient binderCli ...@@ -341,6 +380,14 @@ func syncClaim(volumeIndex *persistentVolumeOrderedIndex, binderClient binderCli
return nil return nil
} }
func isBeingProvisioned(volume *api.PersistentVolume) bool {
value, found := volume.Annotations[pvProvisioningRequiredAnnotationKey]
if found && value != pvProvisioningCompletedAnnotationValue {
return true
}
return false
}
// Run starts all of this binder's control loops // Run starts all of this binder's control loops
func (controller *PersistentVolumeClaimBinder) Run() { func (controller *PersistentVolumeClaimBinder) Run() {
glog.V(5).Infof("Starting PersistentVolumeClaimBinder\n") glog.V(5).Infof("Starting PersistentVolumeClaimBinder\n")
......
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package persistentvolume
import (
"fmt"
"testing"
"time"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/api/unversioned"
client "k8s.io/kubernetes/pkg/client/unversioned"
fake_cloud "k8s.io/kubernetes/pkg/cloudprovider/providers/fake"
"k8s.io/kubernetes/pkg/util"
"k8s.io/kubernetes/pkg/volume"
"k8s.io/kubernetes/pkg/watch"
)
func TestProvisionerRunStop(t *testing.T) {
controller, _ := makeTestController()
if len(controller.stopChannels) != 0 {
t.Errorf("Non-running provisioner should not have any stopChannels. Got %v", len(controller.stopChannels))
}
controller.Run()
if len(controller.stopChannels) != 2 {
t.Errorf("Running provisioner should have exactly 2 stopChannels. Got %v", len(controller.stopChannels))
}
controller.Stop()
if len(controller.stopChannels) != 0 {
t.Errorf("Non-running provisioner should not have any stopChannels. Got %v", len(controller.stopChannels))
}
}
func makeTestVolume() *api.PersistentVolume {
return &api.PersistentVolume{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{},
Name: "pv01",
},
Spec: api.PersistentVolumeSpec{
PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimDelete,
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
Capacity: api.ResourceList{
api.ResourceName(api.ResourceStorage): resource.MustParse("10Gi"),
},
PersistentVolumeSource: api.PersistentVolumeSource{
HostPath: &api.HostPathVolumeSource{
Path: "/tmp/data01",
},
},
},
}
}
func makeTestClaim() *api.PersistentVolumeClaim {
return &api.PersistentVolumeClaim{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{},
Name: "claim01",
Namespace: "ns",
SelfLink: testapi.Default.SelfLink("pvc", ""),
},
Spec: api.PersistentVolumeClaimSpec{
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
Resources: api.ResourceRequirements{
Requests: api.ResourceList{
api.ResourceName(api.ResourceStorage): resource.MustParse("8G"),
},
},
},
}
}
func makeTestController() (*PersistentVolumeProvisionerController, *mockControllerClient) {
mockClient := &mockControllerClient{}
mockVolumePlugin := &volume.FakeVolumePlugin{}
controller, _ := NewPersistentVolumeProvisionerController(mockClient, 1*time.Second, nil, mockVolumePlugin, &fake_cloud.FakeCloud{})
return controller, mockClient
}
func TestReconcileClaim(t *testing.T) {
controller, mockClient := makeTestController()
pvc := makeTestClaim()
// watch would have added the claim to the store
controller.claimStore.Add(pvc)
err := controller.reconcileClaim(pvc)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
// non-provisionable PVC should not have created a volume on reconciliation
if mockClient.volume != nil {
t.Error("Unexpected volume found in mock client. Expected nil")
}
pvc.Annotations[qosProvisioningKey] = "foo"
err = controller.reconcileClaim(pvc)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
// PVC requesting provisioning should have a PV created for it
if mockClient.volume == nil {
t.Error("Expected to find bound volume but got nil")
}
if mockClient.volume.Spec.ClaimRef.Name != pvc.Name {
t.Errorf("Expected PV to be bound to %s but got %s", mockClient.volume.Spec.ClaimRef.Name, pvc.Name)
}
}
func TestReconcileVolume(t *testing.T) {
controller, mockClient := makeTestController()
pv := makeTestVolume()
pvc := makeTestClaim()
err := controller.reconcileVolume(pv)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
// watch adds claim to the store.
// we need to add it to our mock client to mimic normal Get call
controller.claimStore.Add(pvc)
mockClient.claim = pvc
// pretend the claim and volume are bound, no provisioning required
claimRef, _ := api.GetReference(pvc)
pv.Spec.ClaimRef = claimRef
err = controller.reconcileVolume(pv)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
pv.Annotations[pvProvisioningRequiredAnnotationKey] = "!pvProvisioningCompleted"
pv.Annotations[qosProvisioningKey] = "foo"
err = controller.reconcileVolume(pv)
if !isAnnotationMatch(pvProvisioningRequiredAnnotationKey, pvProvisioningCompletedAnnotationValue, mockClient.volume.Annotations) {
t.Errorf("Expected %s but got %s", pvProvisioningRequiredAnnotationKey, mockClient.volume.Annotations[pvProvisioningRequiredAnnotationKey])
}
}
var _ controllerClient = &mockControllerClient{}
type mockControllerClient struct {
volume *api.PersistentVolume
claim *api.PersistentVolumeClaim
}
func (c *mockControllerClient) GetPersistentVolume(name string) (*api.PersistentVolume, error) {
return c.volume, nil
}
func (c *mockControllerClient) CreatePersistentVolume(pv *api.PersistentVolume) (*api.PersistentVolume, error) {
if pv.GenerateName != "" && pv.Name == "" {
pv.Name = fmt.Sprintf(pv.GenerateName, util.NewUUID())
}
c.volume = pv
return c.volume, nil
}
func (c *mockControllerClient) ListPersistentVolumes(options unversioned.ListOptions) (*api.PersistentVolumeList, error) {
return &api.PersistentVolumeList{
Items: []api.PersistentVolume{*c.volume},
}, nil
}
func (c *mockControllerClient) WatchPersistentVolumes(options unversioned.ListOptions) (watch.Interface, error) {
return watch.NewFake(), nil
}
func (c *mockControllerClient) UpdatePersistentVolume(pv *api.PersistentVolume) (*api.PersistentVolume, error) {
return c.CreatePersistentVolume(pv)
}
func (c *mockControllerClient) DeletePersistentVolume(volume *api.PersistentVolume) error {
c.volume = nil
return nil
}
func (c *mockControllerClient) UpdatePersistentVolumeStatus(volume *api.PersistentVolume) (*api.PersistentVolume, error) {
return volume, nil
}
func (c *mockControllerClient) GetPersistentVolumeClaim(namespace, name string) (*api.PersistentVolumeClaim, error) {
if c.claim != nil {
return c.claim, nil
} else {
return nil, errors.NewNotFound("persistentVolume", name)
}
}
func (c *mockControllerClient) ListPersistentVolumeClaims(namespace string, options unversioned.ListOptions) (*api.PersistentVolumeClaimList, error) {
return &api.PersistentVolumeClaimList{
Items: []api.PersistentVolumeClaim{*c.claim},
}, nil
}
func (c *mockControllerClient) WatchPersistentVolumeClaims(namespace string, options unversioned.ListOptions) (watch.Interface, error) {
return watch.NewFake(), nil
}
func (c *mockControllerClient) UpdatePersistentVolumeClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) {
c.claim = claim
return c.claim, nil
}
func (c *mockControllerClient) UpdatePersistentVolumeClaimStatus(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) {
return claim, nil
}
func (c *mockControllerClient) GetKubeClient() client.Interface {
return nil
}
...@@ -46,14 +46,16 @@ type PersistentVolumeRecycler struct { ...@@ -46,14 +46,16 @@ type PersistentVolumeRecycler struct {
client recyclerClient client recyclerClient
kubeClient client.Interface kubeClient client.Interface
pluginMgr volume.VolumePluginMgr pluginMgr volume.VolumePluginMgr
cloud cloudprovider.Interface
} }
// PersistentVolumeRecycler creates a new PersistentVolumeRecycler // PersistentVolumeRecycler creates a new PersistentVolumeRecycler
func NewPersistentVolumeRecycler(kubeClient client.Interface, syncPeriod time.Duration, plugins []volume.VolumePlugin) (*PersistentVolumeRecycler, error) { func NewPersistentVolumeRecycler(kubeClient client.Interface, syncPeriod time.Duration, plugins []volume.VolumePlugin, cloud cloudprovider.Interface) (*PersistentVolumeRecycler, error) {
recyclerClient := NewRecyclerClient(kubeClient) recyclerClient := NewRecyclerClient(kubeClient)
recycler := &PersistentVolumeRecycler{ recycler := &PersistentVolumeRecycler{
client: recyclerClient, client: recyclerClient,
kubeClient: kubeClient, kubeClient: kubeClient,
cloud: cloud,
} }
if err := recycler.pluginMgr.InitPlugins(plugins, recycler); err != nil { if err := recycler.pluginMgr.InitPlugins(plugins, recycler); err != nil {
...@@ -283,7 +285,7 @@ func (f *PersistentVolumeRecycler) NewWrapperCleaner(spec *volume.Spec, podUID t ...@@ -283,7 +285,7 @@ func (f *PersistentVolumeRecycler) NewWrapperCleaner(spec *volume.Spec, podUID t
} }
func (f *PersistentVolumeRecycler) GetCloudProvider() cloudprovider.Interface { func (f *PersistentVolumeRecycler) GetCloudProvider() cloudprovider.Interface {
return nil return f.cloud
} }
func (f *PersistentVolumeRecycler) GetMounter() mount.Interface { func (f *PersistentVolumeRecycler) GetMounter() mount.Interface {
......
...@@ -21,15 +21,15 @@ import ( ...@@ -21,15 +21,15 @@ import (
"sort" "sort"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/client/cache" "k8s.io/kubernetes/pkg/client/cache"
) )
const ( const (
// A PV created specifically for one claim must contain this annotation in order to bind to the claim. // A PVClaim can request a quality of service tier by adding this annotation. The value of the annotation
// The value must be the namespace and name of the claim being bound to (i.e, claim.Namespace/claim.Name) // is arbitrary. The values are pre-defined by a cluster admin and known to users when requesting a QoS.
// This is an experimental feature and likely to change in the future. // For example tiers might be gold, silver, and tin and the admin configures what that means for each volume plugin that can provision a volume.
createdForKey = "volume.extensions.kubernetes.io/provisioned-for" // Values in the alpha version of this feature are not meaningful, but will be in the full version of this feature.
qosProvisioningKey = "volume.alpha.kubernetes.io/storage-class"
) )
// persistentVolumeOrderedIndex is a cache.Store that keeps persistent volumes indexed by AccessModes and ordered by storage capacity. // persistentVolumeOrderedIndex is a cache.Store that keeps persistent volumes indexed by AccessModes and ordered by storage capacity.
...@@ -80,10 +80,7 @@ func (pvIndex *persistentVolumeOrderedIndex) ListByAccessModes(modes []api.Persi ...@@ -80,10 +80,7 @@ func (pvIndex *persistentVolumeOrderedIndex) ListByAccessModes(modes []api.Persi
type matchPredicate func(compareThis, toThis *api.PersistentVolume) bool type matchPredicate func(compareThis, toThis *api.PersistentVolume) bool
// find returns the nearest PV from the ordered list or nil if a match is not found // find returns the nearest PV from the ordered list or nil if a match is not found
func (pvIndex *persistentVolumeOrderedIndex) find(searchPV *api.PersistentVolume, matchPredicate matchPredicate) (*api.PersistentVolume, error) { func (pvIndex *persistentVolumeOrderedIndex) findByClaim(claim *api.PersistentVolumeClaim, matchPredicate matchPredicate) (*api.PersistentVolume, error) {
// the 'searchPV' argument is a synthetic PV with capacity and accessmodes set according to the user's PersistentVolumeClaim.
// the synthetic pv arg is, therefore, a request for a storage resource.
//
// PVs are indexed by their access modes to allow easier searching. Each index is the string representation of a set of access modes. // PVs are indexed by their access modes to allow easier searching. Each index is the string representation of a set of access modes.
// There is a finite number of possible sets and PVs will only be indexed in one of them (whichever index matches the PV's modes). // There is a finite number of possible sets and PVs will only be indexed in one of them (whichever index matches the PV's modes).
// //
...@@ -92,17 +89,7 @@ func (pvIndex *persistentVolumeOrderedIndex) find(searchPV *api.PersistentVolume ...@@ -92,17 +89,7 @@ func (pvIndex *persistentVolumeOrderedIndex) find(searchPV *api.PersistentVolume
// //
// Searches are performed against a set of access modes, so we can attempt not only the exact matching modes but also // Searches are performed against a set of access modes, so we can attempt not only the exact matching modes but also
// potential matches (the GCEPD example above). // potential matches (the GCEPD example above).
allPossibleModes := pvIndex.allPossibleMatchingAccessModes(searchPV.Spec.AccessModes) allPossibleModes := pvIndex.allPossibleMatchingAccessModes(claim.Spec.AccessModes)
// the searchPV should contain an annotation that allows pre-binding to a claim.
// we can use the same annotation value (pvc's namespace/name) and check against
// existing volumes to find an exact match. It is possible that a bind is made (ClaimRef persisted to PV)
// but the fail to update claim.Spec.VolumeName fails. This check allows the claim to find the volume
// that's already bound to the claim.
preboundClaim := ""
if createdFor, ok := searchPV.Annotations[createdForKey]; ok {
preboundClaim = createdFor
}
for _, modes := range allPossibleModes { for _, modes := range allPossibleModes {
volumes, err := pvIndex.ListByAccessModes(modes) volumes, err := pvIndex.ListByAccessModes(modes)
...@@ -115,48 +102,45 @@ func (pvIndex *persistentVolumeOrderedIndex) find(searchPV *api.PersistentVolume ...@@ -115,48 +102,45 @@ func (pvIndex *persistentVolumeOrderedIndex) find(searchPV *api.PersistentVolume
// return the exact pre-binding match, if found // return the exact pre-binding match, if found
unboundVolumes := []*api.PersistentVolume{} unboundVolumes := []*api.PersistentVolume{}
for _, volume := range volumes { for _, volume := range volumes {
if volume.Spec.ClaimRef == nil {
// volume isn't currently bound or pre-bound. // volume isn't currently bound or pre-bound.
if volume.Spec.ClaimRef == nil {
unboundVolumes = append(unboundVolumes, volume) unboundVolumes = append(unboundVolumes, volume)
continue continue
} }
boundClaim := fmt.Sprintf("%s/%s", volume.Spec.ClaimRef.Namespace, volume.Spec.ClaimRef.Name) if claim.Name == volume.Spec.ClaimRef.Name && claim.Namespace == volume.Spec.ClaimRef.Namespace {
if boundClaim == preboundClaim {
// exact match! No search required. // exact match! No search required.
return volume, nil return volume, nil
} }
} }
i := sort.Search(len(unboundVolumes), func(i int) bool { return matchPredicate(searchPV, unboundVolumes[i]) }) // a claim requesting provisioning will have an exact match pre-bound to the claim.
if i < len(unboundVolumes) { // no need to search through unbound volumes. The matching volume will be created by the provisioner
return unboundVolumes[i], nil // and will match above when the claim is re-processed by the binder.
} if keyExists(qosProvisioningKey, claim.Annotations) {
}
return nil, nil return nil, nil
} }
// findByAccessModesAndStorageCapacity is a convenience method that calls Find w/ requisite matchPredicate for storage searchPV := &api.PersistentVolume{
func (pvIndex *persistentVolumeOrderedIndex) findByAccessModesAndStorageCapacity(prebindKey string, modes []api.PersistentVolumeAccessMode, qty resource.Quantity) (*api.PersistentVolume, error) {
pv := &api.PersistentVolume{
ObjectMeta: api.ObjectMeta{
Annotations: map[string]string{
createdForKey: prebindKey,
},
},
Spec: api.PersistentVolumeSpec{ Spec: api.PersistentVolumeSpec{
AccessModes: modes, AccessModes: claim.Spec.AccessModes,
Capacity: api.ResourceList{ Capacity: api.ResourceList{
api.ResourceName(api.ResourceStorage): qty, api.ResourceName(api.ResourceStorage): claim.Spec.Resources.Requests[api.ResourceName(api.ResourceStorage)],
}, },
}, },
} }
return pvIndex.find(pv, matchStorageCapacity)
i := sort.Search(len(unboundVolumes), func(i int) bool { return matchPredicate(searchPV, unboundVolumes[i]) })
if i < len(unboundVolumes) {
return unboundVolumes[i], nil
}
}
return nil, nil
} }
// findBestMatchForClaim is a convenience method that finds a volume by the claim's AccessModes and requests for Storage // findBestMatchForClaim is a convenience method that finds a volume by the claim's AccessModes and requests for Storage
func (pvIndex *persistentVolumeOrderedIndex) findBestMatchForClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolume, error) { func (pvIndex *persistentVolumeOrderedIndex) findBestMatchForClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolume, error) {
return pvIndex.findByAccessModesAndStorageCapacity(fmt.Sprintf("%s/%s", claim.Namespace, claim.Name), claim.Spec.AccessModes, claim.Spec.Resources.Requests[api.ResourceName(api.ResourceStorage)]) return pvIndex.findByClaim(claim, matchStorageCapacity)
} }
// byCapacity is used to order volumes by ascending storage size // byCapacity is used to order volumes by ascending storage size
...@@ -268,3 +252,7 @@ func (c byAccessModes) Swap(i, j int) { ...@@ -268,3 +252,7 @@ func (c byAccessModes) Swap(i, j int) {
func (c byAccessModes) Len() int { func (c byAccessModes) Len() int {
return len(c.modes) return len(c.modes)
} }
func claimToClaimKey(claim *api.PersistentVolumeClaim) string {
return fmt.Sprintf("%s/%s", claim.Namespace, claim.Name)
}
...@@ -22,7 +22,6 @@ import ( ...@@ -22,7 +22,6 @@ import (
"regexp" "regexp"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util"
"k8s.io/kubernetes/pkg/volume" "k8s.io/kubernetes/pkg/volume"
...@@ -38,7 +37,7 @@ func ProbeVolumePlugins(volumeConfig volume.VolumeConfig) []volume.VolumePlugin ...@@ -38,7 +37,7 @@ func ProbeVolumePlugins(volumeConfig volume.VolumeConfig) []volume.VolumePlugin
host: nil, host: nil,
newRecyclerFunc: newRecycler, newRecyclerFunc: newRecycler,
newDeleterFunc: newDeleter, newDeleterFunc: newDeleter,
newCreaterFunc: newCreater, newProvisionerFunc: newProvisioner,
config: volumeConfig, config: volumeConfig,
}, },
} }
...@@ -49,7 +48,7 @@ func ProbeRecyclableVolumePlugins(recyclerFunc func(spec *volume.Spec, host volu ...@@ -49,7 +48,7 @@ func ProbeRecyclableVolumePlugins(recyclerFunc func(spec *volume.Spec, host volu
&hostPathPlugin{ &hostPathPlugin{
host: nil, host: nil,
newRecyclerFunc: recyclerFunc, newRecyclerFunc: recyclerFunc,
newCreaterFunc: newCreater, newProvisionerFunc: newProvisioner,
config: volumeConfig, config: volumeConfig,
}, },
} }
...@@ -57,10 +56,10 @@ func ProbeRecyclableVolumePlugins(recyclerFunc func(spec *volume.Spec, host volu ...@@ -57,10 +56,10 @@ func ProbeRecyclableVolumePlugins(recyclerFunc func(spec *volume.Spec, host volu
type hostPathPlugin struct { type hostPathPlugin struct {
host volume.VolumeHost host volume.VolumeHost
// decouple creating Recyclers/Deleters/Creaters by deferring to a function. Allows for easier testing. // decouple creating Recyclers/Deleters/Provisioners by deferring to a function. Allows for easier testing.
newRecyclerFunc func(spec *volume.Spec, host volume.VolumeHost, volumeConfig volume.VolumeConfig) (volume.Recycler, error) newRecyclerFunc func(spec *volume.Spec, host volume.VolumeHost, volumeConfig volume.VolumeConfig) (volume.Recycler, error)
newDeleterFunc func(spec *volume.Spec, host volume.VolumeHost) (volume.Deleter, error) newDeleterFunc func(spec *volume.Spec, host volume.VolumeHost) (volume.Deleter, error)
newCreaterFunc func(options volume.VolumeOptions, host volume.VolumeHost) (volume.Creater, error) newProvisionerFunc func(options volume.VolumeOptions, host volume.VolumeHost) (volume.Provisioner, error)
config volume.VolumeConfig config volume.VolumeConfig
} }
...@@ -68,7 +67,7 @@ var _ volume.VolumePlugin = &hostPathPlugin{} ...@@ -68,7 +67,7 @@ var _ volume.VolumePlugin = &hostPathPlugin{}
var _ volume.PersistentVolumePlugin = &hostPathPlugin{} var _ volume.PersistentVolumePlugin = &hostPathPlugin{}
var _ volume.RecyclableVolumePlugin = &hostPathPlugin{} var _ volume.RecyclableVolumePlugin = &hostPathPlugin{}
var _ volume.DeletableVolumePlugin = &hostPathPlugin{} var _ volume.DeletableVolumePlugin = &hostPathPlugin{}
var _ volume.CreatableVolumePlugin = &hostPathPlugin{} var _ volume.ProvisionableVolumePlugin = &hostPathPlugin{}
const ( const (
hostPathPluginName = "kubernetes.io/host-path" hostPathPluginName = "kubernetes.io/host-path"
...@@ -124,11 +123,11 @@ func (plugin *hostPathPlugin) NewDeleter(spec *volume.Spec) (volume.Deleter, err ...@@ -124,11 +123,11 @@ func (plugin *hostPathPlugin) NewDeleter(spec *volume.Spec) (volume.Deleter, err
return plugin.newDeleterFunc(spec, plugin.host) return plugin.newDeleterFunc(spec, plugin.host)
} }
func (plugin *hostPathPlugin) NewCreater(options volume.VolumeOptions) (volume.Creater, error) { func (plugin *hostPathPlugin) NewProvisioner(options volume.VolumeOptions) (volume.Provisioner, error) {
if len(options.AccessModes) == 0 { if len(options.AccessModes) == 0 {
options.AccessModes = plugin.GetAccessModes() options.AccessModes = plugin.GetAccessModes()
} }
return plugin.newCreaterFunc(options, plugin.host) return plugin.newProvisionerFunc(options, plugin.host)
} }
func newRecycler(spec *volume.Spec, host volume.VolumeHost, config volume.VolumeConfig) (volume.Recycler, error) { func newRecycler(spec *volume.Spec, host volume.VolumeHost, config volume.VolumeConfig) (volume.Recycler, error) {
...@@ -154,8 +153,8 @@ func newDeleter(spec *volume.Spec, host volume.VolumeHost) (volume.Deleter, erro ...@@ -154,8 +153,8 @@ func newDeleter(spec *volume.Spec, host volume.VolumeHost) (volume.Deleter, erro
return &hostPathDeleter{spec.Name(), path, host, volume.NewMetricsDu(path)}, nil return &hostPathDeleter{spec.Name(), path, host, volume.NewMetricsDu(path)}, nil
} }
func newCreater(options volume.VolumeOptions, host volume.VolumeHost) (volume.Creater, error) { func newProvisioner(options volume.VolumeOptions, host volume.VolumeHost) (volume.Provisioner, error) {
return &hostPathCreater{options: options, host: host}, nil return &hostPathProvisioner{options: options, host: host}, nil
} }
// HostPath volumes represent a bare host file or directory mount. // HostPath volumes represent a bare host file or directory mount.
...@@ -215,7 +214,7 @@ func (c *hostPathCleaner) TearDownAt(dir string) error { ...@@ -215,7 +214,7 @@ func (c *hostPathCleaner) TearDownAt(dir string) error {
return fmt.Errorf("TearDownAt() does not make sense for host paths") return fmt.Errorf("TearDownAt() does not make sense for host paths")
} }
// hostPathRecycler implements a dynamic provisioning Recycler for the HostPath plugin // hostPathRecycler implements a Recycler for the HostPath plugin
// This implementation is meant for testing only and only works in a single node cluster // This implementation is meant for testing only and only works in a single node cluster
type hostPathRecycler struct { type hostPathRecycler struct {
name string name string
...@@ -246,34 +245,36 @@ func (r *hostPathRecycler) Recycle() error { ...@@ -246,34 +245,36 @@ func (r *hostPathRecycler) Recycle() error {
return volume.RecycleVolumeByWatchingPodUntilCompletion(pod, r.host.GetKubeClient()) return volume.RecycleVolumeByWatchingPodUntilCompletion(pod, r.host.GetKubeClient())
} }
// hostPathCreater implements a dynamic provisioning Creater for the HostPath plugin // hostPathProvisioner implements a Provisioner for the HostPath plugin
// This implementation is meant for testing only and only works in a single node cluster. // This implementation is meant for testing only and only works in a single node cluster.
type hostPathCreater struct { type hostPathProvisioner struct {
host volume.VolumeHost host volume.VolumeHost
options volume.VolumeOptions options volume.VolumeOptions
} }
// Create for hostPath simply creates a local /tmp/hostpath_pv/%s directory as a new PersistentVolume. // Create for hostPath simply creates a local /tmp/hostpath_pv/%s directory as a new PersistentVolume.
// This Creater is meant for development and testing only and WILL NOT WORK in a multi-node cluster. // This Provisioner is meant for development and testing only and WILL NOT WORK in a multi-node cluster.
func (r *hostPathCreater) Create() (*api.PersistentVolume, error) { func (r *hostPathProvisioner) Provision(pv *api.PersistentVolume) error {
fullpath := fmt.Sprintf("/tmp/hostpath_pv/%s", util.NewUUID()) if pv.Spec.HostPath == nil {
err := os.MkdirAll(fullpath, 0750) return fmt.Errorf("pv.Spec.HostPath cannot be nil")
if err != nil {
return nil, err
} }
return os.MkdirAll(pv.Spec.HostPath.Path, 0750)
}
func (r *hostPathProvisioner) NewPersistentVolumeTemplate() (*api.PersistentVolume, error) {
fullpath := fmt.Sprintf("/tmp/hostpath_pv/%s", util.NewUUID())
return &api.PersistentVolume{ return &api.PersistentVolume{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
GenerateName: "pv-hostpath-", GenerateName: "pv-hostpath-",
Labels: map[string]string{ Annotations: map[string]string{
"createdby": "hostpath dynamic provisioner", "kubernetes.io/createdby": "hostpath-dynamic-provisioner",
}, },
}, },
Spec: api.PersistentVolumeSpec{ Spec: api.PersistentVolumeSpec{
PersistentVolumeReclaimPolicy: r.options.PersistentVolumeReclaimPolicy, PersistentVolumeReclaimPolicy: r.options.PersistentVolumeReclaimPolicy,
AccessModes: r.options.AccessModes, AccessModes: r.options.AccessModes,
Capacity: api.ResourceList{ Capacity: api.ResourceList{
api.ResourceName(api.ResourceStorage): resource.MustParse(fmt.Sprintf("%dMi", r.options.CapacityMB)), api.ResourceName(api.ResourceStorage): r.options.Capacity,
}, },
PersistentVolumeSource: api.PersistentVolumeSource{ PersistentVolumeSource: api.PersistentVolumeSource{
HostPath: &api.HostPathVolumeSource{ HostPath: &api.HostPathVolumeSource{
......
...@@ -145,7 +145,7 @@ func TestDeleterTempDir(t *testing.T) { ...@@ -145,7 +145,7 @@ func TestDeleterTempDir(t *testing.T) {
} }
} }
func TestCreater(t *testing.T) { func TestProvisioner(t *testing.T) {
tempPath := "/tmp/hostpath/" tempPath := "/tmp/hostpath/"
defer os.RemoveAll(tempPath) defer os.RemoveAll(tempPath)
err := os.MkdirAll(tempPath, 0750) err := os.MkdirAll(tempPath, 0750)
...@@ -157,18 +157,18 @@ func TestCreater(t *testing.T) { ...@@ -157,18 +157,18 @@ func TestCreater(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Can't find the plugin by name") t.Errorf("Can't find the plugin by name")
} }
creater, err := plug.NewCreater(volume.VolumeOptions{CapacityMB: 100, PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimDelete}) creater, err := plug.NewProvisioner(volume.VolumeOptions{Capacity: resource.MustParse("1Gi"), PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimDelete})
if err != nil { if err != nil {
t.Errorf("Failed to make a new Creater: %v", err) t.Errorf("Failed to make a new Provisioner: %v", err)
} }
pv, err := creater.Create() pv, err := creater.NewPersistentVolumeTemplate()
if err != nil { if err != nil {
t.Errorf("Unexpected error creating volume: %v", err) t.Errorf("Unexpected error creating volume: %v", err)
} }
if pv.Spec.HostPath.Path == "" { if pv.Spec.HostPath.Path == "" {
t.Errorf("Expected pv.Spec.HostPath.Path to not be empty: %#v", pv) t.Errorf("Expected pv.Spec.HostPath.Path to not be empty: %#v", pv)
} }
expectedCapacity := resource.NewQuantity(100*1024*1024, resource.BinarySI) expectedCapacity := resource.NewQuantity(1*1024*1024*1024, resource.BinarySI)
actualCapacity := pv.Spec.Capacity[api.ResourceStorage] actualCapacity := pv.Spec.Capacity[api.ResourceStorage]
expectedAmt := expectedCapacity.Value() expectedAmt := expectedCapacity.Value()
actualAmt := actualCapacity.Value() actualAmt := actualCapacity.Value()
......
...@@ -23,6 +23,7 @@ import ( ...@@ -23,6 +23,7 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/resource"
client "k8s.io/kubernetes/pkg/client/unversioned" client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/cloudprovider" "k8s.io/kubernetes/pkg/cloudprovider"
"k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/types"
...@@ -39,11 +40,11 @@ type VolumeOptions struct { ...@@ -39,11 +40,11 @@ type VolumeOptions struct {
// it will be replaced and expanded on by future SecurityContext work. // it will be replaced and expanded on by future SecurityContext work.
RootContext string RootContext string
// The attributes below are required by volume.Creater // The attributes below are required by volume.Provisioner
// perhaps CreaterVolumeOptions struct? // TODO: refactor all of this out of volumes when an admin can configure many kinds of provisioners.
// CapacityMB is the size in MB of a volume. // Capacity is the size of a volume.
CapacityMB int Capacity resource.Quantity
// AccessModes of a volume // AccessModes of a volume
AccessModes []api.PersistentVolumeAccessMode AccessModes []api.PersistentVolumeAccessMode
// Reclamation policy for a persistent volume // Reclamation policy for a persistent volume
...@@ -106,12 +107,12 @@ type DeletableVolumePlugin interface { ...@@ -106,12 +107,12 @@ type DeletableVolumePlugin interface {
NewDeleter(spec *Spec) (Deleter, error) NewDeleter(spec *Spec) (Deleter, error)
} }
// CreatableVolumePlugin is an extended interface of VolumePlugin and is used to create volumes for the cluster. // ProvisionableVolumePlugin is an extended interface of VolumePlugin and is used to create volumes for the cluster.
type CreatableVolumePlugin interface { type ProvisionableVolumePlugin interface {
VolumePlugin VolumePlugin
// NewCreater creates a new volume.Creater which knows how to create PersistentVolumes in accordance with // NewProvisioner creates a new volume.Provisioner which knows how to create PersistentVolumes in accordance with
// the plugin's underlying storage provider // the plugin's underlying storage provider
NewCreater(options VolumeOptions) (Creater, error) NewProvisioner(options VolumeOptions) (Provisioner, error)
} }
// VolumeHost is an interface that plugins can use to access the kubelet. // VolumeHost is an interface that plugins can use to access the kubelet.
...@@ -365,13 +366,13 @@ func (pm *VolumePluginMgr) FindDeletablePluginBySpec(spec *Spec) (DeletableVolum ...@@ -365,13 +366,13 @@ func (pm *VolumePluginMgr) FindDeletablePluginBySpec(spec *Spec) (DeletableVolum
// FindCreatablePluginBySpec fetches a persistent volume plugin by name. If no plugin // FindCreatablePluginBySpec fetches a persistent volume plugin by name. If no plugin
// is found, returns error. // is found, returns error.
func (pm *VolumePluginMgr) FindCreatablePluginBySpec(spec *Spec) (CreatableVolumePlugin, error) { func (pm *VolumePluginMgr) FindCreatablePluginBySpec(spec *Spec) (ProvisionableVolumePlugin, error) {
volumePlugin, err := pm.FindPluginBySpec(spec) volumePlugin, err := pm.FindPluginBySpec(spec)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if creatableVolumePlugin, ok := volumePlugin.(CreatableVolumePlugin); ok { if provisionableVolumePlugin, ok := volumePlugin.(ProvisionableVolumePlugin); ok {
return creatableVolumePlugin, nil return provisionableVolumePlugin, nil
} }
return nil, fmt.Errorf("no creatable volume plugin matched") return nil, fmt.Errorf("no creatable volume plugin matched")
} }
......
...@@ -117,10 +117,13 @@ func ProbeVolumePlugins(config VolumeConfig) []VolumePlugin { ...@@ -117,10 +117,13 @@ func ProbeVolumePlugins(config VolumeConfig) []VolumePlugin {
type FakeVolumePlugin struct { type FakeVolumePlugin struct {
PluginName string PluginName string
Host VolumeHost Host VolumeHost
Config VolumeConfig
} }
var _ VolumePlugin = &FakeVolumePlugin{} var _ VolumePlugin = &FakeVolumePlugin{}
var _ RecyclableVolumePlugin = &FakeVolumePlugin{} var _ RecyclableVolumePlugin = &FakeVolumePlugin{}
var _ DeletableVolumePlugin = &FakeVolumePlugin{}
var _ ProvisionableVolumePlugin = &FakeVolumePlugin{}
func (plugin *FakeVolumePlugin) Init(host VolumeHost) { func (plugin *FakeVolumePlugin) Init(host VolumeHost) {
plugin.Host = host plugin.Host = host
...@@ -151,6 +154,10 @@ func (plugin *FakeVolumePlugin) NewDeleter(spec *Spec) (Deleter, error) { ...@@ -151,6 +154,10 @@ func (plugin *FakeVolumePlugin) NewDeleter(spec *Spec) (Deleter, error) {
return &FakeDeleter{"/attributesTransferredFromSpec", MetricsNil{}}, nil return &FakeDeleter{"/attributesTransferredFromSpec", MetricsNil{}}, nil
} }
func (plugin *FakeVolumePlugin) NewProvisioner(options VolumeOptions) (Provisioner, error) {
return &FakeProvisioner{options, plugin.Host}, nil
}
func (plugin *FakeVolumePlugin) GetAccessModes() []api.PersistentVolumeAccessMode { func (plugin *FakeVolumePlugin) GetAccessModes() []api.PersistentVolumeAccessMode {
return []api.PersistentVolumeAccessMode{} return []api.PersistentVolumeAccessMode{}
} }
...@@ -227,3 +234,36 @@ func (fd *FakeDeleter) Delete() error { ...@@ -227,3 +234,36 @@ func (fd *FakeDeleter) Delete() error {
func (fd *FakeDeleter) GetPath() string { func (fd *FakeDeleter) GetPath() string {
return fd.path return fd.path
} }
type FakeProvisioner struct {
Options VolumeOptions
Host VolumeHost
}
func (fc *FakeProvisioner) NewPersistentVolumeTemplate() (*api.PersistentVolume, error) {
fullpath := fmt.Sprintf("/tmp/hostpath_pv/%s", util.NewUUID())
return &api.PersistentVolume{
ObjectMeta: api.ObjectMeta{
GenerateName: "pv-fakeplugin-",
Annotations: map[string]string{
"kubernetes.io/createdby": "fakeplugin-provisioner",
},
},
Spec: api.PersistentVolumeSpec{
PersistentVolumeReclaimPolicy: fc.Options.PersistentVolumeReclaimPolicy,
AccessModes: fc.Options.AccessModes,
Capacity: api.ResourceList{
api.ResourceName(api.ResourceStorage): fc.Options.Capacity,
},
PersistentVolumeSource: api.PersistentVolumeSource{
HostPath: &api.HostPathVolumeSource{
Path: fullpath,
},
},
},
}, nil
}
func (fc *FakeProvisioner) Provision(pv *api.PersistentVolume) error {
return nil
}
...@@ -100,10 +100,16 @@ type Recycler interface { ...@@ -100,10 +100,16 @@ type Recycler interface {
Recycle() error Recycle() error
} }
// Create adds a new resource in the storage provider and creates a PersistentVolume for the new resource. // Provisioner is an interface that creates templates for PersistentVolumes and can create the volume
// Calls to Create should block until complete. // as a new resource in the infrastructure provider.
type Creater interface { type Provisioner interface {
Create() (*api.PersistentVolume, error) // Provision creates the resource by allocating the underlying volume in a storage system.
// This method should block until completion.
Provision(*api.PersistentVolume) error
// NewPersistentVolumeTemplate creates a new PersistentVolume to be used as a template before saving.
// The provisioner will want to tweak its properties, assign correct annotations, etc.
// This func should *NOT* persist the PV in the API. That is left to the caller.
NewPersistentVolumeTemplate() (*api.PersistentVolume, error)
} }
// Delete removes the resource from the underlying storage provider. Calls to this method should block until // Delete removes the resource from the underlying storage provider. Calls to this method should block until
...@@ -111,6 +117,7 @@ type Creater interface { ...@@ -111,6 +117,7 @@ type Creater interface {
// A nil return indicates success. // A nil return indicates success.
type Deleter interface { type Deleter interface {
Volume Volume
// This method should block until completion.
Delete() error Delete() error
} }
......
...@@ -29,6 +29,7 @@ import ( ...@@ -29,6 +29,7 @@ import (
"k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/unversioned"
client "k8s.io/kubernetes/pkg/client/unversioned" client "k8s.io/kubernetes/pkg/client/unversioned"
fake_cloud "k8s.io/kubernetes/pkg/cloudprovider/providers/fake"
persistentvolumecontroller "k8s.io/kubernetes/pkg/controller/persistentvolume" persistentvolumecontroller "k8s.io/kubernetes/pkg/controller/persistentvolume"
"k8s.io/kubernetes/pkg/conversion" "k8s.io/kubernetes/pkg/conversion"
"k8s.io/kubernetes/pkg/volume" "k8s.io/kubernetes/pkg/volume"
...@@ -48,12 +49,16 @@ func TestPersistentVolumeRecycler(t *testing.T) { ...@@ -48,12 +49,16 @@ func TestPersistentVolumeRecycler(t *testing.T) {
binderClient := client.NewOrDie(&client.Config{Host: s.URL, GroupVersion: testapi.Default.GroupVersion()}) binderClient := client.NewOrDie(&client.Config{Host: s.URL, GroupVersion: testapi.Default.GroupVersion()})
recyclerClient := client.NewOrDie(&client.Config{Host: s.URL, GroupVersion: testapi.Default.GroupVersion()}) recyclerClient := client.NewOrDie(&client.Config{Host: s.URL, GroupVersion: testapi.Default.GroupVersion()})
testClient := client.NewOrDie(&client.Config{Host: s.URL, GroupVersion: testapi.Default.GroupVersion()}) testClient := client.NewOrDie(&client.Config{Host: s.URL, GroupVersion: testapi.Default.GroupVersion()})
host := volume.NewFakeVolumeHost("/tmp/fake", nil, nil)
binder := persistentvolumecontroller.NewPersistentVolumeClaimBinder(binderClient, 10*time.Minute) plugins := []volume.VolumePlugin{&volume.FakeVolumePlugin{"plugin-name", host, volume.VolumeConfig{}}}
cloud := &fake_cloud.FakeCloud{}
binder := persistentvolumecontroller.NewPersistentVolumeClaimBinder(binderClient, 10*time.Second)
binder.Run() binder.Run()
defer binder.Stop() defer binder.Stop()
recycler, _ := persistentvolumecontroller.NewPersistentVolumeRecycler(recyclerClient, 30*time.Minute, []volume.VolumePlugin{&volume.FakeVolumePlugin{"plugin-name", volume.NewFakeVolumeHost("/tmp/fake", nil, nil)}}) recycler, _ := persistentvolumecontroller.NewPersistentVolumeRecycler(recyclerClient, 30*time.Second, plugins, cloud)
recycler.Run() recycler.Run()
defer recycler.Stop() defer recycler.Stop()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment