diff --git a/pkg/controller/persistentvolume/persistentvolume_index_test.go b/pkg/controller/persistentvolume/persistentvolume_index_test.go index 143c74a81e3..338d4d2efbe 100644 --- a/pkg/controller/persistentvolume/persistentvolume_index_test.go +++ b/pkg/controller/persistentvolume/persistentvolume_index_test.go @@ -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) + } +} diff --git a/pkg/controller/persistentvolume/types.go b/pkg/controller/persistentvolume/types.go index bcce67039e4..31c5d72cbfe 100644 --- a/pkg/controller/persistentvolume/types.go +++ b/pkg/controller/persistentvolume/types.go @@ -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