allow pre-binding of persistent volumes to pvclaims

pull/6/head
markturansky 2015-09-22 22:38:25 -04:00
parent efedcb6ca1
commit 09600095c4
2 changed files with 115 additions and 12 deletions

View File

@ -484,3 +484,78 @@ func createTestVolumes() []*api.PersistentVolume {
},
}
}
func TestFindingPreboundVolumes(t *testing.T) {
pv1 := &api.PersistentVolume{
ObjectMeta: api.ObjectMeta{
Name: "pv1",
Annotations: map[string]string{},
},
Spec: api.PersistentVolumeSpec{
Capacity: api.ResourceList{api.ResourceName(api.ResourceStorage): resource.MustParse("1Gi")},
PersistentVolumeSource: api.PersistentVolumeSource{HostPath: &api.HostPathVolumeSource{}},
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
},
}
pv5 := &api.PersistentVolume{
ObjectMeta: api.ObjectMeta{
Name: "pv5",
Annotations: map[string]string{},
},
Spec: api.PersistentVolumeSpec{
Capacity: api.ResourceList{api.ResourceName(api.ResourceStorage): resource.MustParse("5Gi")},
PersistentVolumeSource: api.PersistentVolumeSource{HostPath: &api.HostPathVolumeSource{}},
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
},
}
pv8 := &api.PersistentVolume{
ObjectMeta: api.ObjectMeta{
Name: "pv8",
Annotations: map[string]string{},
},
Spec: api.PersistentVolumeSpec{
Capacity: api.ResourceList{api.ResourceName(api.ResourceStorage): resource.MustParse("8Gi")},
PersistentVolumeSource: api.PersistentVolumeSource{HostPath: &api.HostPathVolumeSource{}},
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
},
}
claim := &api.PersistentVolumeClaim{
ObjectMeta: api.ObjectMeta{
Name: "claim01",
Namespace: "myns",
},
Spec: api.PersistentVolumeClaimSpec{
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
Resources: api.ResourceRequirements{Requests: api.ResourceList{api.ResourceName(api.ResourceStorage): resource.MustParse("1Gi")}},
},
}
index := NewPersistentVolumeOrderedIndex()
index.Add(pv1)
index.Add(pv5)
index.Add(pv8)
// expected exact match on size
volume, _ := index.FindBestMatchForClaim(claim)
if volume.Name != pv1.Name {
t.Errorf("Expected %s but got volume %s instead", pv1.Name, volume.Name)
}
// pretend the exact match is pre-bound. should get the next size up.
pv1.Annotations[createdForKey] = "some/other/claim"
volume, _ = index.FindBestMatchForClaim(claim)
if volume.Name != pv5.Name {
t.Errorf("Expected %s but got volume %s instead", pv5.Name, volume.Name)
}
// pretend the exact match is available but the largest volume is pre-bound to the claim.
delete(pv1.Annotations, createdForKey)
pv8.Annotations[createdForKey] = "myns/claim01"
volume, _ = index.FindBestMatchForClaim(claim)
if volume.Name != pv8.Name {
t.Errorf("Expected %s but got volume %s instead", pv8.Name, volume.Name)
}
}

View File

@ -25,6 +25,13 @@ import (
"k8s.io/kubernetes/pkg/client/cache"
)
const (
// A PV created specifically for one claim must contain this annotation in order to bind to the claim.
// The value must be the name of the claim being bound to.
// This is an experimental feature and likely to change in the future.
createdForKey = "persistent-volume-provisioning.experimental.kubernetes.io/created-for"
)
// persistentVolumeOrderedIndex is a cache.Store that keeps persistent volumes indexed by AccessModes and ordered by storage capacity.
type persistentVolumeOrderedIndex struct {
cache.Indexer
@ -73,8 +80,8 @@ func (pvIndex *persistentVolumeOrderedIndex) ListByAccessModes(modes []api.Persi
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
func (pvIndex *persistentVolumeOrderedIndex) Find(pv *api.PersistentVolume, matchPredicate matchPredicate) (*api.PersistentVolume, error) {
// the 'pv' argument is a synthetic PV with capacity and accessmodes set according to the user's PersistentVolumeClaim.
func (pvIndex *persistentVolumeOrderedIndex) Find(searchPV *api.PersistentVolume, 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.
@ -85,7 +92,7 @@ func (pvIndex *persistentVolumeOrderedIndex) Find(pv *api.PersistentVolume, matc
//
// 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).
allPossibleModes := pvIndex.allPossibleMatchingAccessModes(pv.Spec.AccessModes)
allPossibleModes := pvIndex.allPossibleMatchingAccessModes(searchPV.Spec.AccessModes)
for _, modes := range allPossibleModes {
volumes, err := pvIndex.ListByAccessModes(modes)
@ -93,16 +100,32 @@ func (pvIndex *persistentVolumeOrderedIndex) Find(pv *api.PersistentVolume, matc
return nil, err
}
// volumes are sorted by size but some may be bound.
// remove bound volumes for easy binary search by size
// volumes are sorted by size but some may be bound or earmarked for a specific claim.
// filter those volumes for easy binary search by size
// return the exact pre-binding match, if found
unboundVolumes := []*api.PersistentVolume{}
for _, v := range volumes {
if v.Spec.ClaimRef == nil {
unboundVolumes = append(unboundVolumes, v)
for _, volume := range volumes {
// check for current binding
if volume.Spec.ClaimRef != nil {
continue
}
// check for pre-bind where the volume is intended for one specific claim
if createdFor, ok := volume.Annotations[createdForKey]; ok {
if createdFor != searchPV.Annotations[createdForKey] {
// the volume is pre-bound and does not match the search criteria.
continue
} else {
// exact annotation match! No search required.
return volume, nil
}
}
// volume isn't currently bound or pre-bound.
unboundVolumes = append(unboundVolumes, volume)
}
i := sort.Search(len(unboundVolumes), func(i int) bool { return matchPredicate(pv, unboundVolumes[i]) })
i := sort.Search(len(unboundVolumes), func(i int) bool { return matchPredicate(searchPV, unboundVolumes[i]) })
if i < len(unboundVolumes) {
return unboundVolumes[i], nil
}
@ -110,9 +133,14 @@ func (pvIndex *persistentVolumeOrderedIndex) Find(pv *api.PersistentVolume, matc
return nil, nil
}
// FindByAccessModesAndStorageCapacity is a convenience method that calls Find w/ requisite matchPredicate for storage
func (pvIndex *persistentVolumeOrderedIndex) FindByAccessModesAndStorageCapacity(modes []api.PersistentVolumeAccessMode, qty resource.Quantity) (*api.PersistentVolume, error) {
// findByAccessModesAndStorageCapacity is a convenience method that calls Find w/ requisite matchPredicate for storage
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{
AccessModes: modes,
Capacity: api.ResourceList{
@ -125,7 +153,7 @@ func (pvIndex *persistentVolumeOrderedIndex) FindByAccessModesAndStorageCapacity
// 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) {
return pvIndex.FindByAccessModesAndStorageCapacity(claim.Spec.AccessModes, claim.Spec.Resources.Requests[api.ResourceName(api.ResourceStorage)])
return pvIndex.findByAccessModesAndStorageCapacity(fmt.Sprintf("%s/%s", claim.Namespace, claim.Name), claim.Spec.AccessModes, claim.Spec.Resources.Requests[api.ResourceName(api.ResourceStorage)])
}
// byCapacity is used to order volumes by ascending storage size