mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-27 21:47:07 +00:00
In findMatchingVolumes:
- Delay binding until Pod is scheduled - Check for PV node affinity and access modes - Exclude volumes from being matching
This commit is contained in:
parent
3d0b510e25
commit
58823a75a4
@ -29,4 +29,5 @@ const (
|
|||||||
ProvisioningFailed = "ProvisioningFailed"
|
ProvisioningFailed = "ProvisioningFailed"
|
||||||
ProvisioningCleanupFailed = "ProvisioningCleanupFailed"
|
ProvisioningCleanupFailed = "ProvisioningCleanupFailed"
|
||||||
ProvisioningSucceeded = "ProvisioningSucceeded"
|
ProvisioningSucceeded = "ProvisioningSucceeded"
|
||||||
|
WaitForFirstConsumer = "WaitForFirstConsumer"
|
||||||
)
|
)
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
"k8s.io/api/core/v1"
|
"k8s.io/api/core/v1"
|
||||||
storage "k8s.io/api/storage/v1"
|
storage "k8s.io/api/storage/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -178,6 +179,25 @@ func TestSync(t *testing.T) {
|
|||||||
[]string{"Normal FailedBinding"},
|
[]string{"Normal FailedBinding"},
|
||||||
noerrors, testSyncClaim,
|
noerrors, testSyncClaim,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// syncClaim does not do anything when binding is delayed
|
||||||
|
"1-13 - delayed binding",
|
||||||
|
newVolumeArray("volume1-1", "1Gi", "", "", v1.VolumePending, v1.PersistentVolumeReclaimRetain, classWait),
|
||||||
|
newVolumeArray("volume1-1", "1Gi", "", "", v1.VolumePending, v1.PersistentVolumeReclaimRetain, classWait),
|
||||||
|
newClaimArray("claim1-1", "uid1-1", "1Gi", "", v1.ClaimPending, &classWait),
|
||||||
|
newClaimArray("claim1-1", "uid1-1", "1Gi", "", v1.ClaimPending, &classWait),
|
||||||
|
[]string{"Normal WaitForFirstConsumer"},
|
||||||
|
noerrors, testSyncClaim,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// syncClaim binds when binding is delayed but PV is prebound to PVC
|
||||||
|
"1-14 - successful prebound PV",
|
||||||
|
newVolumeArray("volume1-1", "1Gi", "", "claim1-1", v1.VolumePending, v1.PersistentVolumeReclaimRetain, classWait),
|
||||||
|
newVolumeArray("volume1-1", "1Gi", "uid1-1", "claim1-1", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classWait),
|
||||||
|
newClaimArray("claim1-1", "uid1-1", "1Gi", "", v1.ClaimPending, &classWait),
|
||||||
|
newClaimArray("claim1-1", "uid1-1", "1Gi", "volume1-1", v1.ClaimBound, &classWait, annBoundByController, annBindCompleted),
|
||||||
|
noevents, noerrors, testSyncClaim,
|
||||||
|
},
|
||||||
|
|
||||||
// [Unit test set 2] User asked for a specific PV.
|
// [Unit test set 2] User asked for a specific PV.
|
||||||
// Test the binding when pv.ClaimRef is already set by controller or
|
// Test the binding when pv.ClaimRef is already set by controller or
|
||||||
@ -570,7 +590,15 @@ func TestSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
runSyncTests(t, tests, []*storage.StorageClass{})
|
utilfeature.DefaultFeatureGate.Set("VolumeScheduling=true")
|
||||||
|
defer utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false")
|
||||||
|
|
||||||
|
runSyncTests(t, tests, []*storage.StorageClass{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: classWait},
|
||||||
|
VolumeBindingMode: &modeWait,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSyncAlphaBlockVolume(t *testing.T) {
|
func TestSyncAlphaBlockVolume(t *testing.T) {
|
||||||
|
@ -822,6 +822,9 @@ var (
|
|||||||
classUnknownInternal string = "unknown-internal"
|
classUnknownInternal string = "unknown-internal"
|
||||||
classUnsupportedMountOptions string = "unsupported-mountoptions"
|
classUnsupportedMountOptions string = "unsupported-mountoptions"
|
||||||
classLarge string = "large"
|
classLarge string = "large"
|
||||||
|
classWait string = "wait"
|
||||||
|
|
||||||
|
modeWait = storage.VolumeBindingWaitForFirstConsumer
|
||||||
)
|
)
|
||||||
|
|
||||||
// wrapTestWithPluginCalls returns a testCall that:
|
// wrapTestWithPluginCalls returns a testCall that:
|
||||||
|
@ -29,6 +29,7 @@ import (
|
|||||||
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
|
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
"k8s.io/kubernetes/pkg/features"
|
||||||
"k8s.io/kubernetes/pkg/volume"
|
"k8s.io/kubernetes/pkg/volume"
|
||||||
|
volumeutil "k8s.io/kubernetes/pkg/volume/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// persistentVolumeOrderedIndex is a cache.Store that keeps persistent volumes
|
// persistentVolumeOrderedIndex is a cache.Store that keeps persistent volumes
|
||||||
@ -74,7 +75,7 @@ func (pvIndex *persistentVolumeOrderedIndex) listByAccessModes(modes []v1.Persis
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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) findByClaim(claim *v1.PersistentVolumeClaim) (*v1.PersistentVolume, error) {
|
func (pvIndex *persistentVolumeOrderedIndex) findByClaim(claim *v1.PersistentVolumeClaim, delayBinding bool) (*v1.PersistentVolume, error) {
|
||||||
// PVs are indexed by their access modes to allow easier searching. Each
|
// 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
|
// 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
|
// finite number of possible sets and PVs will only be indexed in one of
|
||||||
@ -96,7 +97,7 @@ func (pvIndex *persistentVolumeOrderedIndex) findByClaim(claim *v1.PersistentVol
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
bestVol, err := findMatchingVolume(claim, volumes)
|
bestVol, err := findMatchingVolume(claim, volumes, nil /* node for topology binding*/, nil /* exclusion map */, delayBinding)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -108,7 +109,27 @@ func (pvIndex *persistentVolumeOrderedIndex) findByClaim(claim *v1.PersistentVol
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findMatchingVolume(claim *v1.PersistentVolumeClaim, volumes []*v1.PersistentVolume) (*v1.PersistentVolume, error) {
|
// findMatchingVolume goes through the list of volumes to find the best matching volume
|
||||||
|
// for the claim.
|
||||||
|
//
|
||||||
|
// This function is used by both the PV controller and scheduler.
|
||||||
|
//
|
||||||
|
// delayBinding is true only in the PV controller path. When set, prebound PVs are still returned
|
||||||
|
// as a match for the claim, but unbound PVs are skipped.
|
||||||
|
//
|
||||||
|
// node is set only in the scheduler path. When set, the PV node affinity is checked against
|
||||||
|
// the node's labels.
|
||||||
|
//
|
||||||
|
// excludedVolumes is only used in the scheduler path, and is needed for evaluating multiple
|
||||||
|
// unbound PVCs for a single Pod at one time. As each PVC finds a matching PV, the chosen
|
||||||
|
// PV needs to be excluded from future matching.
|
||||||
|
func findMatchingVolume(
|
||||||
|
claim *v1.PersistentVolumeClaim,
|
||||||
|
volumes []*v1.PersistentVolume,
|
||||||
|
node *v1.Node,
|
||||||
|
excludedVolumes map[string]*v1.PersistentVolume,
|
||||||
|
delayBinding bool) (*v1.PersistentVolume, error) {
|
||||||
|
|
||||||
var smallestVolume *v1.PersistentVolume
|
var smallestVolume *v1.PersistentVolume
|
||||||
var smallestVolumeQty resource.Quantity
|
var smallestVolumeQty resource.Quantity
|
||||||
requestedQty := claim.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)]
|
requestedQty := claim.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)]
|
||||||
@ -131,6 +152,11 @@ func findMatchingVolume(claim *v1.PersistentVolumeClaim, volumes []*v1.Persisten
|
|||||||
// - find the smallest matching one if there is no volume pre-bound to
|
// - find the smallest matching one if there is no volume pre-bound to
|
||||||
// the claim.
|
// the claim.
|
||||||
for _, volume := range volumes {
|
for _, volume := range volumes {
|
||||||
|
if _, ok := excludedVolumes[volume.Name]; ok {
|
||||||
|
// Skip volumes in the excluded list
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
volumeQty := volume.Spec.Capacity[v1.ResourceStorage]
|
volumeQty := volume.Spec.Capacity[v1.ResourceStorage]
|
||||||
|
|
||||||
// check if volumeModes do not match (Alpha and feature gate protected)
|
// check if volumeModes do not match (Alpha and feature gate protected)
|
||||||
@ -143,6 +169,15 @@ func findMatchingVolume(claim *v1.PersistentVolumeClaim, volumes []*v1.Persisten
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if node != nil {
|
||||||
|
// Scheduler path, check that the PV NodeAffinity
|
||||||
|
// is satisfied by the node
|
||||||
|
err := volumeutil.CheckNodeAffinity(volume, node.Labels)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if isVolumeBoundToClaim(volume, claim) {
|
if isVolumeBoundToClaim(volume, claim) {
|
||||||
// this claim and volume are pre-bound; return
|
// this claim and volume are pre-bound; return
|
||||||
// the volume if the size request is satisfied,
|
// the volume if the size request is satisfied,
|
||||||
@ -153,6 +188,13 @@ func findMatchingVolume(claim *v1.PersistentVolumeClaim, volumes []*v1.Persisten
|
|||||||
return volume, nil
|
return volume, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if node == nil && delayBinding {
|
||||||
|
// PV controller does not bind this claim.
|
||||||
|
// Scheduler will handle binding unbound volumes
|
||||||
|
// Scheduler path will have node != nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// filter out:
|
// filter out:
|
||||||
// - volumes bound to another claim
|
// - volumes bound to another claim
|
||||||
// - volumes whose labels don't match the claim's selector, if specified
|
// - volumes whose labels don't match the claim's selector, if specified
|
||||||
@ -166,6 +208,14 @@ func findMatchingVolume(claim *v1.PersistentVolumeClaim, volumes []*v1.Persisten
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if node != nil {
|
||||||
|
// Scheduler path
|
||||||
|
// Check that the access modes match
|
||||||
|
if !checkAccessModes(claim, volume) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if volumeQty.Cmp(requestedQty) >= 0 {
|
if volumeQty.Cmp(requestedQty) >= 0 {
|
||||||
if smallestVolume == nil || smallestVolumeQty.Cmp(volumeQty) > 0 {
|
if smallestVolume == nil || smallestVolumeQty.Cmp(volumeQty) > 0 {
|
||||||
smallestVolume = volume
|
smallestVolume = volume
|
||||||
@ -204,8 +254,8 @@ func checkVolumeModeMisMatches(pvcSpec *v1.PersistentVolumeClaimSpec, pvSpec *v1
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 *v1.PersistentVolumeClaim) (*v1.PersistentVolume, error) {
|
func (pvIndex *persistentVolumeOrderedIndex) findBestMatchForClaim(claim *v1.PersistentVolumeClaim, delayBinding bool) (*v1.PersistentVolume, error) {
|
||||||
return pvIndex.findByClaim(claim)
|
return pvIndex.findByClaim(claim, delayBinding)
|
||||||
}
|
}
|
||||||
|
|
||||||
// allPossibleMatchingAccessModes returns an array of AccessMode arrays that
|
// allPossibleMatchingAccessModes returns an array of AccessMode arrays that
|
||||||
@ -287,3 +337,19 @@ func claimToClaimKey(claim *v1.PersistentVolumeClaim) string {
|
|||||||
func claimrefToClaimKey(claimref *v1.ObjectReference) string {
|
func claimrefToClaimKey(claimref *v1.ObjectReference) string {
|
||||||
return fmt.Sprintf("%s/%s", claimref.Namespace, claimref.Name)
|
return fmt.Sprintf("%s/%s", claimref.Namespace, claimref.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true if PV satisfies all the PVC's requested AccessModes
|
||||||
|
func checkAccessModes(claim *v1.PersistentVolumeClaim, volume *v1.PersistentVolume) bool {
|
||||||
|
pvModesMap := map[v1.PersistentVolumeAccessMode]bool{}
|
||||||
|
for _, mode := range volume.Spec.AccessModes {
|
||||||
|
pvModesMap[mode] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mode := range claim.Spec.AccessModes {
|
||||||
|
_, ok := pvModesMap[mode]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
@ -20,6 +20,8 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
|
||||||
"k8s.io/api/core/v1"
|
"k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/resource"
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -27,6 +29,7 @@ import (
|
|||||||
"k8s.io/client-go/kubernetes/scheme"
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
ref "k8s.io/client-go/tools/reference"
|
ref "k8s.io/client-go/tools/reference"
|
||||||
"k8s.io/kubernetes/pkg/api/testapi"
|
"k8s.io/kubernetes/pkg/api/testapi"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/core/v1/helper"
|
||||||
"k8s.io/kubernetes/pkg/volume"
|
"k8s.io/kubernetes/pkg/volume"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -178,7 +181,7 @@ func TestMatchVolume(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for name, scenario := range scenarios {
|
for name, scenario := range scenarios {
|
||||||
volume, err := volList.findBestMatchForClaim(scenario.claim)
|
volume, err := volList.findBestMatchForClaim(scenario.claim, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error matching volume by claim: %v", err)
|
t.Errorf("Unexpected error matching volume by claim: %v", err)
|
||||||
}
|
}
|
||||||
@ -249,7 +252,7 @@ func TestMatchingWithBoundVolumes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
volume, err := volumeIndex.findBestMatchForClaim(claim)
|
volume, err := volumeIndex.findBestMatchForClaim(claim, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error matching volume by claim: %v", err)
|
t.Fatalf("Unexpected error matching volume by claim: %v", err)
|
||||||
}
|
}
|
||||||
@ -372,27 +375,27 @@ func TestFindingVolumeWithDifferentAccessModes(t *testing.T) {
|
|||||||
index.store.Add(ebs)
|
index.store.Add(ebs)
|
||||||
index.store.Add(nfs)
|
index.store.Add(nfs)
|
||||||
|
|
||||||
volume, _ := index.findBestMatchForClaim(claim)
|
volume, _ := index.findBestMatchForClaim(claim, false)
|
||||||
if volume.Name != ebs.Name {
|
if volume.Name != ebs.Name {
|
||||||
t.Errorf("Expected %s but got volume %s instead", ebs.Name, volume.Name)
|
t.Errorf("Expected %s but got volume %s instead", ebs.Name, volume.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
claim.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce, v1.ReadOnlyMany}
|
claim.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce, v1.ReadOnlyMany}
|
||||||
volume, _ = index.findBestMatchForClaim(claim)
|
volume, _ = index.findBestMatchForClaim(claim, false)
|
||||||
if volume.Name != gce.Name {
|
if volume.Name != gce.Name {
|
||||||
t.Errorf("Expected %s but got volume %s instead", gce.Name, volume.Name)
|
t.Errorf("Expected %s but got volume %s instead", gce.Name, volume.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// order of the requested modes should not matter
|
// order of the requested modes should not matter
|
||||||
claim.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteMany, v1.ReadWriteOnce, v1.ReadOnlyMany}
|
claim.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteMany, v1.ReadWriteOnce, v1.ReadOnlyMany}
|
||||||
volume, _ = index.findBestMatchForClaim(claim)
|
volume, _ = index.findBestMatchForClaim(claim, false)
|
||||||
if volume.Name != nfs.Name {
|
if volume.Name != nfs.Name {
|
||||||
t.Errorf("Expected %s but got volume %s instead", nfs.Name, volume.Name)
|
t.Errorf("Expected %s but got volume %s instead", nfs.Name, volume.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fewer modes requested should still match
|
// fewer modes requested should still match
|
||||||
claim.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteMany}
|
claim.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteMany}
|
||||||
volume, _ = index.findBestMatchForClaim(claim)
|
volume, _ = index.findBestMatchForClaim(claim, false)
|
||||||
if volume.Name != nfs.Name {
|
if volume.Name != nfs.Name {
|
||||||
t.Errorf("Expected %s but got volume %s instead", nfs.Name, volume.Name)
|
t.Errorf("Expected %s but got volume %s instead", nfs.Name, volume.Name)
|
||||||
}
|
}
|
||||||
@ -400,7 +403,7 @@ func TestFindingVolumeWithDifferentAccessModes(t *testing.T) {
|
|||||||
// pretend the exact match is bound. should get the next level up of modes.
|
// pretend the exact match is bound. should get the next level up of modes.
|
||||||
ebs.Spec.ClaimRef = &v1.ObjectReference{}
|
ebs.Spec.ClaimRef = &v1.ObjectReference{}
|
||||||
claim.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
|
claim.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
|
||||||
volume, _ = index.findBestMatchForClaim(claim)
|
volume, _ = index.findBestMatchForClaim(claim, false)
|
||||||
if volume.Name != gce.Name {
|
if volume.Name != gce.Name {
|
||||||
t.Errorf("Expected %s but got volume %s instead", gce.Name, volume.Name)
|
t.Errorf("Expected %s but got volume %s instead", gce.Name, volume.Name)
|
||||||
}
|
}
|
||||||
@ -408,7 +411,7 @@ func TestFindingVolumeWithDifferentAccessModes(t *testing.T) {
|
|||||||
// continue up the levels of modes.
|
// continue up the levels of modes.
|
||||||
gce.Spec.ClaimRef = &v1.ObjectReference{}
|
gce.Spec.ClaimRef = &v1.ObjectReference{}
|
||||||
claim.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
|
claim.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
|
||||||
volume, _ = index.findBestMatchForClaim(claim)
|
volume, _ = index.findBestMatchForClaim(claim, false)
|
||||||
if volume.Name != nfs.Name {
|
if volume.Name != nfs.Name {
|
||||||
t.Errorf("Expected %s but got volume %s instead", nfs.Name, volume.Name)
|
t.Errorf("Expected %s but got volume %s instead", nfs.Name, volume.Name)
|
||||||
}
|
}
|
||||||
@ -416,7 +419,7 @@ func TestFindingVolumeWithDifferentAccessModes(t *testing.T) {
|
|||||||
// partial mode request
|
// partial mode request
|
||||||
gce.Spec.ClaimRef = nil
|
gce.Spec.ClaimRef = nil
|
||||||
claim.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadOnlyMany}
|
claim.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadOnlyMany}
|
||||||
volume, _ = index.findBestMatchForClaim(claim)
|
volume, _ = index.findBestMatchForClaim(claim, false)
|
||||||
if volume.Name != gce.Name {
|
if volume.Name != gce.Name {
|
||||||
t.Errorf("Expected %s but got volume %s instead", gce.Name, volume.Name)
|
t.Errorf("Expected %s but got volume %s instead", gce.Name, volume.Name)
|
||||||
}
|
}
|
||||||
@ -675,6 +678,87 @@ func createTestVolumes() []*v1.PersistentVolume {
|
|||||||
StorageClassName: classLarge,
|
StorageClassName: classLarge,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
UID: "affinity-pv",
|
||||||
|
Name: "affinity001",
|
||||||
|
Annotations: getAnnotationWithNodeAffinity("key1", "value1"),
|
||||||
|
},
|
||||||
|
Spec: v1.PersistentVolumeSpec{
|
||||||
|
Capacity: v1.ResourceList{
|
||||||
|
v1.ResourceName(v1.ResourceStorage): resource.MustParse("100G"),
|
||||||
|
},
|
||||||
|
PersistentVolumeSource: v1.PersistentVolumeSource{
|
||||||
|
Local: &v1.LocalVolumeSource{},
|
||||||
|
},
|
||||||
|
AccessModes: []v1.PersistentVolumeAccessMode{
|
||||||
|
v1.ReadWriteOnce,
|
||||||
|
v1.ReadOnlyMany,
|
||||||
|
},
|
||||||
|
StorageClassName: classWait,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
UID: "affinity-pv2",
|
||||||
|
Name: "affinity002",
|
||||||
|
Annotations: getAnnotationWithNodeAffinity("key1", "value1"),
|
||||||
|
},
|
||||||
|
Spec: v1.PersistentVolumeSpec{
|
||||||
|
Capacity: v1.ResourceList{
|
||||||
|
v1.ResourceName(v1.ResourceStorage): resource.MustParse("150G"),
|
||||||
|
},
|
||||||
|
PersistentVolumeSource: v1.PersistentVolumeSource{
|
||||||
|
Local: &v1.LocalVolumeSource{},
|
||||||
|
},
|
||||||
|
AccessModes: []v1.PersistentVolumeAccessMode{
|
||||||
|
v1.ReadWriteOnce,
|
||||||
|
v1.ReadOnlyMany,
|
||||||
|
},
|
||||||
|
StorageClassName: classWait,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
UID: "affinity-prebound",
|
||||||
|
Name: "affinity003",
|
||||||
|
Annotations: getAnnotationWithNodeAffinity("key1", "value1"),
|
||||||
|
},
|
||||||
|
Spec: v1.PersistentVolumeSpec{
|
||||||
|
Capacity: v1.ResourceList{
|
||||||
|
v1.ResourceName(v1.ResourceStorage): resource.MustParse("100G"),
|
||||||
|
},
|
||||||
|
PersistentVolumeSource: v1.PersistentVolumeSource{
|
||||||
|
Local: &v1.LocalVolumeSource{},
|
||||||
|
},
|
||||||
|
AccessModes: []v1.PersistentVolumeAccessMode{
|
||||||
|
v1.ReadWriteOnce,
|
||||||
|
v1.ReadOnlyMany,
|
||||||
|
},
|
||||||
|
StorageClassName: classWait,
|
||||||
|
ClaimRef: &v1.ObjectReference{Name: "claim02", Namespace: "myns"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
UID: "affinity-pv3",
|
||||||
|
Name: "affinity003",
|
||||||
|
Annotations: getAnnotationWithNodeAffinity("key1", "value3"),
|
||||||
|
},
|
||||||
|
Spec: v1.PersistentVolumeSpec{
|
||||||
|
Capacity: v1.ResourceList{
|
||||||
|
v1.ResourceName(v1.ResourceStorage): resource.MustParse("200G"),
|
||||||
|
},
|
||||||
|
PersistentVolumeSource: v1.PersistentVolumeSource{
|
||||||
|
Local: &v1.LocalVolumeSource{},
|
||||||
|
},
|
||||||
|
AccessModes: []v1.PersistentVolumeAccessMode{
|
||||||
|
v1.ReadWriteOnce,
|
||||||
|
v1.ReadOnlyMany,
|
||||||
|
},
|
||||||
|
StorageClassName: classWait,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -692,6 +776,32 @@ func testVolume(name, size string) *v1.PersistentVolume {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAnnotationWithNodeAffinity(key string, value string) map[string]string {
|
||||||
|
affinity := &v1.NodeAffinity{
|
||||||
|
RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
|
||||||
|
NodeSelectorTerms: []v1.NodeSelectorTerm{
|
||||||
|
{
|
||||||
|
MatchExpressions: []v1.NodeSelectorRequirement{
|
||||||
|
{
|
||||||
|
Key: key,
|
||||||
|
Operator: v1.NodeSelectorOpIn,
|
||||||
|
Values: []string{value},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
annotations := map[string]string{}
|
||||||
|
err := helper.StorageNodeAffinityToAlphaAnnotation(annotations, affinity)
|
||||||
|
if err != nil {
|
||||||
|
glog.Fatalf("Failed to get node affinity annotation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotations
|
||||||
|
}
|
||||||
|
|
||||||
func createVolumeModeBlockTestVolume() *v1.PersistentVolume {
|
func createVolumeModeBlockTestVolume() *v1.PersistentVolume {
|
||||||
blockMode := v1.PersistentVolumeBlock
|
blockMode := v1.PersistentVolumeBlock
|
||||||
|
|
||||||
@ -919,7 +1029,7 @@ func TestAlphaFilteringVolumeModes(t *testing.T) {
|
|||||||
|
|
||||||
for name, scenario := range scenarios {
|
for name, scenario := range scenarios {
|
||||||
toggleBlockVolumeFeature(scenario.enableBlock, t)
|
toggleBlockVolumeFeature(scenario.enableBlock, t)
|
||||||
pvmatch, err := scenario.vol.findBestMatchForClaim(scenario.pvc)
|
pvmatch, err := scenario.vol.findBestMatchForClaim(scenario.pvc, false)
|
||||||
// expected to match but either got an error or no returned pvmatch
|
// expected to match but either got an error or no returned pvmatch
|
||||||
if pvmatch == nil && scenario.isExpectedMatch {
|
if pvmatch == nil && scenario.isExpectedMatch {
|
||||||
t.Errorf("Unexpected failure for scenario, no matching volume: %s", name)
|
t.Errorf("Unexpected failure for scenario, no matching volume: %s", name)
|
||||||
@ -972,14 +1082,14 @@ func TestFindingPreboundVolumes(t *testing.T) {
|
|||||||
index.store.Add(pvBadMode)
|
index.store.Add(pvBadMode)
|
||||||
|
|
||||||
// expected exact match on size
|
// expected exact match on size
|
||||||
volume, _ := index.findBestMatchForClaim(claim)
|
volume, _ := index.findBestMatchForClaim(claim, false)
|
||||||
if volume.Name != pv1.Name {
|
if volume.Name != pv1.Name {
|
||||||
t.Errorf("Expected %s but got volume %s instead", pv1.Name, volume.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.
|
// pretend the exact match is pre-bound. should get the next size up.
|
||||||
pv1.Spec.ClaimRef = &v1.ObjectReference{Name: "foo", Namespace: "bar"}
|
pv1.Spec.ClaimRef = &v1.ObjectReference{Name: "foo", Namespace: "bar"}
|
||||||
volume, _ = index.findBestMatchForClaim(claim)
|
volume, _ = index.findBestMatchForClaim(claim, false)
|
||||||
if volume.Name != pv5.Name {
|
if volume.Name != pv5.Name {
|
||||||
t.Errorf("Expected %s but got volume %s instead", pv5.Name, volume.Name)
|
t.Errorf("Expected %s but got volume %s instead", pv5.Name, volume.Name)
|
||||||
}
|
}
|
||||||
@ -987,7 +1097,7 @@ func TestFindingPreboundVolumes(t *testing.T) {
|
|||||||
// pretend the exact match is available but the largest volume is pre-bound to the claim.
|
// pretend the exact match is available but the largest volume is pre-bound to the claim.
|
||||||
pv1.Spec.ClaimRef = nil
|
pv1.Spec.ClaimRef = nil
|
||||||
pv8.Spec.ClaimRef = claimRef
|
pv8.Spec.ClaimRef = claimRef
|
||||||
volume, _ = index.findBestMatchForClaim(claim)
|
volume, _ = index.findBestMatchForClaim(claim, false)
|
||||||
if volume.Name != pv8.Name {
|
if volume.Name != pv8.Name {
|
||||||
t.Errorf("Expected %s but got volume %s instead", pv8.Name, volume.Name)
|
t.Errorf("Expected %s but got volume %s instead", pv8.Name, volume.Name)
|
||||||
}
|
}
|
||||||
@ -995,7 +1105,7 @@ func TestFindingPreboundVolumes(t *testing.T) {
|
|||||||
// pretend the volume with too small a size is pre-bound to the claim. should get the exact match.
|
// pretend the volume with too small a size is pre-bound to the claim. should get the exact match.
|
||||||
pv8.Spec.ClaimRef = nil
|
pv8.Spec.ClaimRef = nil
|
||||||
pvBadSize.Spec.ClaimRef = claimRef
|
pvBadSize.Spec.ClaimRef = claimRef
|
||||||
volume, _ = index.findBestMatchForClaim(claim)
|
volume, _ = index.findBestMatchForClaim(claim, false)
|
||||||
if volume.Name != pv1.Name {
|
if volume.Name != pv1.Name {
|
||||||
t.Errorf("Expected %s but got volume %s instead", pv1.Name, volume.Name)
|
t.Errorf("Expected %s but got volume %s instead", pv1.Name, volume.Name)
|
||||||
}
|
}
|
||||||
@ -1003,12 +1113,186 @@ func TestFindingPreboundVolumes(t *testing.T) {
|
|||||||
// pretend the volume without the right access mode is pre-bound to the claim. should get the exact match.
|
// pretend the volume without the right access mode is pre-bound to the claim. should get the exact match.
|
||||||
pvBadSize.Spec.ClaimRef = nil
|
pvBadSize.Spec.ClaimRef = nil
|
||||||
pvBadMode.Spec.ClaimRef = claimRef
|
pvBadMode.Spec.ClaimRef = claimRef
|
||||||
volume, _ = index.findBestMatchForClaim(claim)
|
volume, _ = index.findBestMatchForClaim(claim, false)
|
||||||
if volume.Name != pv1.Name {
|
if volume.Name != pv1.Name {
|
||||||
t.Errorf("Expected %s but got volume %s instead", pv1.Name, volume.Name)
|
t.Errorf("Expected %s but got volume %s instead", pv1.Name, volume.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBestMatchDelayed(t *testing.T) {
|
||||||
|
volList := newPersistentVolumeOrderedIndex()
|
||||||
|
for _, pv := range createTestVolumes() {
|
||||||
|
volList.store.Add(pv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// binding through PV controller should be delayed
|
||||||
|
claim := makePVC("8G", nil)
|
||||||
|
volume, err := volList.findBestMatchForClaim(claim, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error matching volume by claim: %v", err)
|
||||||
|
}
|
||||||
|
if volume != nil {
|
||||||
|
t.Errorf("Unexpected match with %q", volume.UID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindMatchVolumeWithNode(t *testing.T) {
|
||||||
|
volumes := createTestVolumes()
|
||||||
|
node1 := &v1.Node{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: map[string]string{"key1": "value1"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
node2 := &v1.Node{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: map[string]string{"key1": "value2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
node3 := &v1.Node{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: map[string]string{"key1": "value3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
expectedMatch string
|
||||||
|
claim *v1.PersistentVolumeClaim
|
||||||
|
node *v1.Node
|
||||||
|
excludedVolumes map[string]*v1.PersistentVolume
|
||||||
|
}{
|
||||||
|
"success-match": {
|
||||||
|
expectedMatch: "affinity-pv",
|
||||||
|
claim: makePVC("100G", func(pvc *v1.PersistentVolumeClaim) {
|
||||||
|
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
|
||||||
|
pvc.Spec.StorageClassName = &classWait
|
||||||
|
}),
|
||||||
|
node: node1,
|
||||||
|
},
|
||||||
|
"success-prebound": {
|
||||||
|
expectedMatch: "affinity-prebound",
|
||||||
|
claim: makePVC("100G", func(pvc *v1.PersistentVolumeClaim) {
|
||||||
|
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
|
||||||
|
pvc.Spec.StorageClassName = &classWait
|
||||||
|
pvc.Name = "claim02"
|
||||||
|
}),
|
||||||
|
node: node1,
|
||||||
|
},
|
||||||
|
"success-exclusion": {
|
||||||
|
expectedMatch: "affinity-pv2",
|
||||||
|
claim: makePVC("100G", func(pvc *v1.PersistentVolumeClaim) {
|
||||||
|
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
|
||||||
|
pvc.Spec.StorageClassName = &classWait
|
||||||
|
}),
|
||||||
|
node: node1,
|
||||||
|
excludedVolumes: map[string]*v1.PersistentVolume{"affinity001": nil},
|
||||||
|
},
|
||||||
|
"fail-exclusion": {
|
||||||
|
expectedMatch: "",
|
||||||
|
claim: makePVC("100G", func(pvc *v1.PersistentVolumeClaim) {
|
||||||
|
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
|
||||||
|
pvc.Spec.StorageClassName = &classWait
|
||||||
|
}),
|
||||||
|
node: node1,
|
||||||
|
excludedVolumes: map[string]*v1.PersistentVolume{"affinity001": nil, "affinity002": nil},
|
||||||
|
},
|
||||||
|
"fail-accessmode": {
|
||||||
|
expectedMatch: "",
|
||||||
|
claim: makePVC("100G", func(pvc *v1.PersistentVolumeClaim) {
|
||||||
|
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteMany}
|
||||||
|
pvc.Spec.StorageClassName = &classWait
|
||||||
|
}),
|
||||||
|
node: node1,
|
||||||
|
},
|
||||||
|
"fail-nodeaffinity": {
|
||||||
|
expectedMatch: "",
|
||||||
|
claim: makePVC("100G", func(pvc *v1.PersistentVolumeClaim) {
|
||||||
|
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
|
||||||
|
pvc.Spec.StorageClassName = &classWait
|
||||||
|
}),
|
||||||
|
node: node2,
|
||||||
|
},
|
||||||
|
"fail-prebound-node-affinity": {
|
||||||
|
expectedMatch: "",
|
||||||
|
claim: makePVC("100G", func(pvc *v1.PersistentVolumeClaim) {
|
||||||
|
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
|
||||||
|
pvc.Spec.StorageClassName = &classWait
|
||||||
|
pvc.Name = "claim02"
|
||||||
|
}),
|
||||||
|
node: node2,
|
||||||
|
},
|
||||||
|
"success-bad-and-good-node-affinity": {
|
||||||
|
expectedMatch: "affinity-pv3",
|
||||||
|
claim: makePVC("100G", func(pvc *v1.PersistentVolumeClaim) {
|
||||||
|
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
|
||||||
|
pvc.Spec.StorageClassName = &classWait
|
||||||
|
pvc.Name = "claim03"
|
||||||
|
}),
|
||||||
|
node: node3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
volume, err := findMatchingVolume(scenario.claim, volumes, scenario.node, scenario.excludedVolumes, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error matching volume by claim: %v", err)
|
||||||
|
}
|
||||||
|
if len(scenario.expectedMatch) != 0 && volume == nil {
|
||||||
|
t.Errorf("Expected match but received nil volume for scenario: %s", name)
|
||||||
|
}
|
||||||
|
if len(scenario.expectedMatch) != 0 && volume != nil && string(volume.UID) != scenario.expectedMatch {
|
||||||
|
t.Errorf("Expected %s but got volume %s in scenario %s", scenario.expectedMatch, volume.UID, name)
|
||||||
|
}
|
||||||
|
if len(scenario.expectedMatch) == 0 && volume != nil {
|
||||||
|
t.Errorf("Unexpected match for scenario: %s, matched with %s instead", name, volume.UID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckAccessModes(t *testing.T) {
|
||||||
|
volume := &v1.PersistentVolume{
|
||||||
|
Spec: v1.PersistentVolumeSpec{
|
||||||
|
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce, v1.ReadWriteMany},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
shouldSucceed bool
|
||||||
|
claim *v1.PersistentVolumeClaim
|
||||||
|
}{
|
||||||
|
"success-single-mode": {
|
||||||
|
shouldSucceed: true,
|
||||||
|
claim: makePVC("100G", func(pvc *v1.PersistentVolumeClaim) {
|
||||||
|
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteMany}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"success-many-modes": {
|
||||||
|
shouldSucceed: true,
|
||||||
|
claim: makePVC("100G", func(pvc *v1.PersistentVolumeClaim) {
|
||||||
|
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteMany, v1.ReadWriteOnce}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"fail-single-mode": {
|
||||||
|
shouldSucceed: false,
|
||||||
|
claim: makePVC("100G", func(pvc *v1.PersistentVolumeClaim) {
|
||||||
|
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadOnlyMany}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"fail-many-modes": {
|
||||||
|
shouldSucceed: false,
|
||||||
|
claim: makePVC("100G", func(pvc *v1.PersistentVolumeClaim) {
|
||||||
|
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteMany, v1.ReadOnlyMany}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
result := checkAccessModes(scenario.claim, volume)
|
||||||
|
if result != scenario.shouldSucceed {
|
||||||
|
t.Errorf("Test %q failed: Expected %v, got %v", name, scenario.shouldSucceed, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// byCapacity is used to order volumes by ascending storage size
|
// byCapacity is used to order volumes by ascending storage size
|
||||||
type byCapacity struct {
|
type byCapacity struct {
|
||||||
volumes []*v1.PersistentVolume
|
volumes []*v1.PersistentVolume
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
storage "k8s.io/api/storage/v1"
|
storage "k8s.io/api/storage/v1"
|
||||||
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/kubernetes/scheme"
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
corelisters "k8s.io/client-go/listers/core/v1"
|
corelisters "k8s.io/client-go/listers/core/v1"
|
||||||
@ -37,6 +38,7 @@ import (
|
|||||||
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
|
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
|
||||||
"k8s.io/kubernetes/pkg/cloudprovider"
|
"k8s.io/kubernetes/pkg/cloudprovider"
|
||||||
"k8s.io/kubernetes/pkg/controller/volume/events"
|
"k8s.io/kubernetes/pkg/controller/volume/events"
|
||||||
|
"k8s.io/kubernetes/pkg/features"
|
||||||
"k8s.io/kubernetes/pkg/util/goroutinemap"
|
"k8s.io/kubernetes/pkg/util/goroutinemap"
|
||||||
"k8s.io/kubernetes/pkg/util/goroutinemap/exponentialbackoff"
|
"k8s.io/kubernetes/pkg/util/goroutinemap/exponentialbackoff"
|
||||||
vol "k8s.io/kubernetes/pkg/volume"
|
vol "k8s.io/kubernetes/pkg/volume"
|
||||||
@ -254,6 +256,30 @@ func checkVolumeSatisfyClaim(volume *v1.PersistentVolume, claim *v1.PersistentVo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctrl *PersistentVolumeController) shouldDelayBinding(claim *v1.PersistentVolumeClaim) (bool, error) {
|
||||||
|
if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeScheduling) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
className := v1helper.GetPersistentVolumeClaimClass(claim)
|
||||||
|
if className == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
class, err := ctrl.classLister.Get(className)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if class.VolumeBindingMode == nil {
|
||||||
|
return false, fmt.Errorf("VolumeBindingMode not set for StorageClass %q", className)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add check to handle dynamic provisioning later
|
||||||
|
|
||||||
|
return *class.VolumeBindingMode == storage.VolumeBindingWaitForFirstConsumer, nil
|
||||||
|
}
|
||||||
|
|
||||||
// syncUnboundClaim is the main controller method to decide what to do with an
|
// syncUnboundClaim is the main controller method to decide what to do with an
|
||||||
// unbound claim.
|
// unbound claim.
|
||||||
func (ctrl *PersistentVolumeController) syncUnboundClaim(claim *v1.PersistentVolumeClaim) error {
|
func (ctrl *PersistentVolumeController) syncUnboundClaim(claim *v1.PersistentVolumeClaim) error {
|
||||||
@ -261,9 +287,13 @@ func (ctrl *PersistentVolumeController) syncUnboundClaim(claim *v1.PersistentVol
|
|||||||
// OBSERVATION: pvc is "Pending"
|
// OBSERVATION: pvc is "Pending"
|
||||||
if claim.Spec.VolumeName == "" {
|
if claim.Spec.VolumeName == "" {
|
||||||
// User did not care which PV they get.
|
// User did not care which PV they get.
|
||||||
|
delayBinding, err := ctrl.shouldDelayBinding(claim)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// [Unit test set 1]
|
// [Unit test set 1]
|
||||||
volume, err := ctrl.volumes.findBestMatchForClaim(claim)
|
volume, err := ctrl.volumes.findBestMatchForClaim(claim, delayBinding)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.V(2).Infof("synchronizing unbound PersistentVolumeClaim[%s]: Error finding PV for claim: %v", claimToClaimKey(claim), err)
|
glog.V(2).Infof("synchronizing unbound PersistentVolumeClaim[%s]: Error finding PV for claim: %v", claimToClaimKey(claim), err)
|
||||||
return fmt.Errorf("Error finding PV for claim %q: %v", claimToClaimKey(claim), err)
|
return fmt.Errorf("Error finding PV for claim %q: %v", claimToClaimKey(claim), err)
|
||||||
@ -272,15 +302,21 @@ func (ctrl *PersistentVolumeController) syncUnboundClaim(claim *v1.PersistentVol
|
|||||||
glog.V(4).Infof("synchronizing unbound PersistentVolumeClaim[%s]: no volume found", claimToClaimKey(claim))
|
glog.V(4).Infof("synchronizing unbound PersistentVolumeClaim[%s]: no volume found", claimToClaimKey(claim))
|
||||||
// No PV could be found
|
// No PV could be found
|
||||||
// OBSERVATION: pvc is "Pending", will retry
|
// OBSERVATION: pvc is "Pending", will retry
|
||||||
if v1helper.GetPersistentVolumeClaimClass(claim) != "" {
|
switch {
|
||||||
|
case delayBinding:
|
||||||
|
// TODO: Skip dynamic provisioning for now
|
||||||
|
ctrl.eventRecorder.Event(claim, v1.EventTypeNormal, events.WaitForFirstConsumer, "waiting for first consumer to be created before binding")
|
||||||
|
case v1helper.GetPersistentVolumeClaimClass(claim) != "":
|
||||||
if err = ctrl.provisionClaim(claim); err != nil {
|
if err = ctrl.provisionClaim(claim); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
default:
|
||||||
|
ctrl.eventRecorder.Event(claim, v1.EventTypeNormal, events.FailedBinding, "no persistent volumes available for this claim and no storage class is set")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark the claim as Pending and try to find a match in the next
|
// Mark the claim as Pending and try to find a match in the next
|
||||||
// periodic syncClaim
|
// periodic syncClaim
|
||||||
ctrl.eventRecorder.Event(claim, v1.EventTypeNormal, events.FailedBinding, "no persistent volumes available for this claim and no storage class is set")
|
|
||||||
if _, err = ctrl.updateClaimStatus(claim, v1.ClaimPending, nil); err != nil {
|
if _, err = ctrl.updateClaimStatus(claim, v1.ClaimPending, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
|
|
||||||
"k8s.io/api/core/v1"
|
"k8s.io/api/core/v1"
|
||||||
|
storagev1 "k8s.io/api/storage/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
core "k8s.io/client-go/testing"
|
core "k8s.io/client-go/testing"
|
||||||
@ -232,3 +236,106 @@ func addVolumeAnnotation(volume *v1.PersistentVolume, annName, annValue string)
|
|||||||
volume.Annotations[annName] = annValue
|
volume.Annotations[annName] = annValue
|
||||||
return volume
|
return volume
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makePVCClass(scName *string) *v1.PersistentVolumeClaim {
|
||||||
|
return &v1.PersistentVolumeClaim{
|
||||||
|
Spec: v1.PersistentVolumeClaimSpec{
|
||||||
|
StorageClassName: scName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeStorageClass(scName string, mode *storagev1.VolumeBindingMode) *storagev1.StorageClass {
|
||||||
|
return &storagev1.StorageClass{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: scName,
|
||||||
|
},
|
||||||
|
VolumeBindingMode: mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelayBinding(t *testing.T) {
|
||||||
|
var (
|
||||||
|
classNotHere = "not-here"
|
||||||
|
classNoMode = "no-mode"
|
||||||
|
classImmediateMode = "immediate-mode"
|
||||||
|
classWaitMode = "wait-mode"
|
||||||
|
|
||||||
|
modeImmediate = storagev1.VolumeBindingImmediate
|
||||||
|
modeWait = storagev1.VolumeBindingWaitForFirstConsumer
|
||||||
|
)
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
pvc *v1.PersistentVolumeClaim
|
||||||
|
shouldDelay bool
|
||||||
|
shouldFail bool
|
||||||
|
}{
|
||||||
|
"nil-class": {
|
||||||
|
pvc: makePVCClass(nil),
|
||||||
|
shouldDelay: false,
|
||||||
|
},
|
||||||
|
"class-not-found": {
|
||||||
|
pvc: makePVCClass(&classNotHere),
|
||||||
|
shouldDelay: false,
|
||||||
|
},
|
||||||
|
"no-mode-class": {
|
||||||
|
pvc: makePVCClass(&classNoMode),
|
||||||
|
shouldDelay: false,
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
"immediate-mode-class": {
|
||||||
|
pvc: makePVCClass(&classImmediateMode),
|
||||||
|
shouldDelay: false,
|
||||||
|
},
|
||||||
|
"wait-mode-class": {
|
||||||
|
pvc: makePVCClass(&classWaitMode),
|
||||||
|
shouldDelay: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
classes := []*storagev1.StorageClass{
|
||||||
|
makeStorageClass(classNoMode, nil),
|
||||||
|
makeStorageClass(classImmediateMode, &modeImmediate),
|
||||||
|
makeStorageClass(classWaitMode, &modeWait),
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &fake.Clientset{}
|
||||||
|
informerFactory := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
|
||||||
|
classInformer := informerFactory.Storage().V1().StorageClasses()
|
||||||
|
ctrl := &PersistentVolumeController{
|
||||||
|
classLister: classInformer.Lister(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, class := range classes {
|
||||||
|
if err := classInformer.Informer().GetIndexer().Add(class); err != nil {
|
||||||
|
t.Fatalf("Failed to add storage class %q: %v", class.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When feature gate is disabled, should always be delayed
|
||||||
|
name := "feature-disabled"
|
||||||
|
shouldDelay, err := ctrl.shouldDelayBinding(makePVCClass(&classWaitMode))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %q returned error: %v", name, err)
|
||||||
|
}
|
||||||
|
if shouldDelay {
|
||||||
|
t.Errorf("Test %q returned true, expected false", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable feature gate
|
||||||
|
utilfeature.DefaultFeatureGate.Set("VolumeScheduling=true")
|
||||||
|
defer utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false")
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
shouldDelay, err = ctrl.shouldDelayBinding(test.pvc)
|
||||||
|
if err != nil && !test.shouldFail {
|
||||||
|
t.Errorf("Test %q returned error: %v", name, err)
|
||||||
|
}
|
||||||
|
if err == nil && test.shouldFail {
|
||||||
|
t.Errorf("Test %q returned success, expected error", name)
|
||||||
|
}
|
||||||
|
if shouldDelay != test.shouldDelay {
|
||||||
|
t.Errorf("Test %q returned unexpected %v", name, test.shouldDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user