
* FindPodVolumes * Prebound PVCs are treated like unbound immediate PVCs and will error * Always check for fully bound PVCs and cache bindings for not fully bound PVCs * BindPodVolumes * Retry API updates for not fully bound PVCs even if the assume cache already marked it * Wait for PVCs to be fully bound after making the API updates * Error when detecting binding/provisioning failure conditions
1488 lines
50 KiB
Go
1488 lines
50 KiB
Go
/*
|
|
Copyright 2017 The Kubernetes Authors.
|
|
|
|
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"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang/glog"
|
|
|
|
"k8s.io/api/core/v1"
|
|
storagev1 "k8s.io/api/storage/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/diff"
|
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
"k8s.io/client-go/informers"
|
|
clientset "k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
"k8s.io/kubernetes/pkg/api/testapi"
|
|
"k8s.io/kubernetes/pkg/controller"
|
|
)
|
|
|
|
var (
|
|
// PVCs for manual binding
|
|
// TODO: clean up all of these
|
|
unboundPVC = makeTestPVC("unbound-pvc", "1G", "", pvcUnbound, "", "1", &waitClass)
|
|
unboundPVC2 = makeTestPVC("unbound-pvc2", "5G", "", pvcUnbound, "", "1", &waitClass)
|
|
preboundPVC = makeTestPVC("prebound-pvc", "1G", "", pvcPrebound, "pv-node1a", "1", &waitClass)
|
|
preboundPVCNode1a = makeTestPVC("unbound-pvc", "1G", "", pvcPrebound, "pv-node1a", "1", &waitClass)
|
|
boundPVC = makeTestPVC("bound-pvc", "1G", "", pvcBound, "pv-bound", "1", &waitClass)
|
|
boundPVC2 = makeTestPVC("bound-pvc2", "1G", "", pvcBound, "pv-bound2", "1", &waitClass)
|
|
boundPVCNode1a = makeTestPVC("unbound-pvc", "1G", "", pvcBound, "pv-node1a", "1", &waitClass)
|
|
badPVC = makeBadPVC()
|
|
immediateUnboundPVC = makeTestPVC("immediate-unbound-pvc", "1G", "", pvcUnbound, "", "1", &immediateClass)
|
|
immediateBoundPVC = makeTestPVC("immediate-bound-pvc", "1G", "", pvcBound, "pv-bound-immediate", "1", &immediateClass)
|
|
|
|
// PVCs for dynamic provisioning
|
|
provisionedPVC = makeTestPVC("provisioned-pvc", "1Gi", "", pvcUnbound, "", "1", &waitClassWithProvisioner)
|
|
provisionedPVC2 = makeTestPVC("provisioned-pvc2", "1Gi", "", pvcUnbound, "", "1", &waitClassWithProvisioner)
|
|
provisionedPVCHigherVersion = makeTestPVC("provisioned-pvc2", "1Gi", "", pvcUnbound, "", "2", &waitClassWithProvisioner)
|
|
provisionedPVCBound = makeTestPVC("provisioned-pvc", "1Gi", "", pvcBound, "some-pv", "1", &waitClassWithProvisioner)
|
|
noProvisionerPVC = makeTestPVC("no-provisioner-pvc", "1Gi", "", pvcUnbound, "", "1", &waitClass)
|
|
topoMismatchPVC = makeTestPVC("topo-mismatch-pvc", "1Gi", "", pvcUnbound, "", "1", &topoMismatchClass)
|
|
|
|
selectedNodePVC = makeTestPVC("provisioned-pvc", "1Gi", nodeLabelValue, pvcSelectedNode, "", "1", &waitClassWithProvisioner)
|
|
|
|
// PVs for manual binding
|
|
pvNoNode = makeTestPV("pv-no-node", "", "1G", "1", nil, waitClass)
|
|
pvNode1a = makeTestPV("pv-node1a", "node1", "5G", "1", nil, waitClass)
|
|
pvNode1b = makeTestPV("pv-node1b", "node1", "10G", "1", nil, waitClass)
|
|
pvNode1c = makeTestPV("pv-node1b", "node1", "5G", "1", nil, waitClass)
|
|
pvNode2 = makeTestPV("pv-node2", "node2", "1G", "1", nil, waitClass)
|
|
pvPrebound = makeTestPV("pv-prebound", "node1", "1G", "1", unboundPVC, waitClass)
|
|
pvBound = makeTestPV("pv-bound", "node1", "1G", "1", boundPVC, waitClass)
|
|
pvNode1aBound = makeTestPV("pv-node1a", "node1", "5G", "1", unboundPVC, waitClass)
|
|
pvNode1bBound = makeTestPV("pv-node1b", "node1", "10G", "1", unboundPVC2, waitClass)
|
|
pvNode1bBoundHigherVersion = makeTestPV("pv-node1b", "node1", "10G", "2", unboundPVC2, waitClass)
|
|
pvBoundImmediate = makeTestPV("pv-bound-immediate", "node1", "1G", "1", immediateBoundPVC, immediateClass)
|
|
pvBoundImmediateNode2 = makeTestPV("pv-bound-immediate", "node2", "1G", "1", immediateBoundPVC, immediateClass)
|
|
|
|
// PVC/PV bindings for manual binding
|
|
binding1a = makeBinding(unboundPVC, pvNode1a)
|
|
binding1b = makeBinding(unboundPVC2, pvNode1b)
|
|
bindingNoNode = makeBinding(unboundPVC, pvNoNode)
|
|
bindingBad = makeBinding(badPVC, pvNode1b)
|
|
binding1aBound = makeBinding(unboundPVC, pvNode1aBound)
|
|
binding1bBound = makeBinding(unboundPVC2, pvNode1bBound)
|
|
|
|
// storage class names
|
|
waitClass = "waitClass"
|
|
immediateClass = "immediateClass"
|
|
waitClassWithProvisioner = "waitClassWithProvisioner"
|
|
topoMismatchClass = "topoMismatchClass"
|
|
|
|
// node topology
|
|
nodeLabelKey = "nodeKey"
|
|
nodeLabelValue = "node1"
|
|
)
|
|
|
|
type testEnv struct {
|
|
client clientset.Interface
|
|
reactor *volumeReactor
|
|
binder SchedulerVolumeBinder
|
|
internalBinder *volumeBinder
|
|
internalPVCache *pvAssumeCache
|
|
internalPVCCache *pvcAssumeCache
|
|
}
|
|
|
|
func newTestBinder(t *testing.T) *testEnv {
|
|
client := &fake.Clientset{}
|
|
reactor := newVolumeReactor(client, nil, nil, nil, nil)
|
|
informerFactory := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
|
|
|
|
pvcInformer := informerFactory.Core().V1().PersistentVolumeClaims()
|
|
classInformer := informerFactory.Storage().V1().StorageClasses()
|
|
|
|
binder := NewVolumeBinder(
|
|
client,
|
|
pvcInformer,
|
|
informerFactory.Core().V1().PersistentVolumes(),
|
|
classInformer,
|
|
10*time.Second)
|
|
|
|
// Add storageclasses
|
|
waitMode := storagev1.VolumeBindingWaitForFirstConsumer
|
|
immediateMode := storagev1.VolumeBindingImmediate
|
|
classes := []*storagev1.StorageClass{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: waitClassWithProvisioner,
|
|
},
|
|
VolumeBindingMode: &waitMode,
|
|
Provisioner: "test-provisioner",
|
|
AllowedTopologies: []v1.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: nodeLabelKey,
|
|
Values: []string{nodeLabelValue, "reference-value"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: immediateClass,
|
|
},
|
|
VolumeBindingMode: &immediateMode,
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: waitClass,
|
|
},
|
|
VolumeBindingMode: &waitMode,
|
|
Provisioner: "kubernetes.io/no-provisioner",
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: topoMismatchClass,
|
|
},
|
|
VolumeBindingMode: &waitMode,
|
|
Provisioner: "test-provisioner",
|
|
AllowedTopologies: []v1.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: nodeLabelKey,
|
|
Values: []string{"reference-value"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, class := range classes {
|
|
if err := classInformer.Informer().GetIndexer().Add(class); err != nil {
|
|
t.Fatalf("Failed to add storage class to internal cache: %v", err)
|
|
}
|
|
}
|
|
|
|
// Get internal types
|
|
internalBinder, ok := binder.(*volumeBinder)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert to internal binder")
|
|
}
|
|
|
|
pvCache := internalBinder.pvCache
|
|
internalPVCache, ok := pvCache.(*pvAssumeCache)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert to internal PV cache")
|
|
}
|
|
|
|
pvcCache := internalBinder.pvcCache
|
|
internalPVCCache, ok := pvcCache.(*pvcAssumeCache)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert to internal PVC cache")
|
|
}
|
|
|
|
return &testEnv{
|
|
client: client,
|
|
reactor: reactor,
|
|
binder: binder,
|
|
internalBinder: internalBinder,
|
|
internalPVCache: internalPVCache,
|
|
internalPVCCache: internalPVCCache,
|
|
}
|
|
}
|
|
|
|
func (env *testEnv) initClaims(cachedPVCs []*v1.PersistentVolumeClaim, apiPVCs []*v1.PersistentVolumeClaim) {
|
|
internalPVCCache := env.internalPVCCache
|
|
for _, pvc := range cachedPVCs {
|
|
internalPVCCache.add(pvc)
|
|
if apiPVCs == nil {
|
|
env.reactor.claims[pvc.Name] = pvc
|
|
}
|
|
}
|
|
for _, pvc := range apiPVCs {
|
|
env.reactor.claims[pvc.Name] = pvc
|
|
}
|
|
}
|
|
|
|
func (env *testEnv) initVolumes(cachedPVs []*v1.PersistentVolume, apiPVs []*v1.PersistentVolume) {
|
|
internalPVCache := env.internalPVCache
|
|
for _, pv := range cachedPVs {
|
|
internalPVCache.add(pv)
|
|
if apiPVs == nil {
|
|
env.reactor.volumes[pv.Name] = pv
|
|
}
|
|
}
|
|
for _, pv := range apiPVs {
|
|
env.reactor.volumes[pv.Name] = pv
|
|
}
|
|
|
|
}
|
|
|
|
func (env *testEnv) assumeVolumes(t *testing.T, name, node string, pod *v1.Pod, bindings []*bindingInfo, provisionings []*v1.PersistentVolumeClaim) {
|
|
pvCache := env.internalBinder.pvCache
|
|
for _, binding := range bindings {
|
|
if err := pvCache.Assume(binding.pv); err != nil {
|
|
t.Fatalf("Failed to setup test %q: error: %v", name, err)
|
|
}
|
|
}
|
|
|
|
env.internalBinder.podBindingCache.UpdateBindings(pod, node, bindings)
|
|
|
|
pvcCache := env.internalBinder.pvcCache
|
|
for _, pvc := range provisionings {
|
|
if err := pvcCache.Assume(pvc); err != nil {
|
|
t.Fatalf("Failed to setup test %q: error: %v", name, err)
|
|
}
|
|
}
|
|
|
|
env.internalBinder.podBindingCache.UpdateProvisionedPVCs(pod, node, provisionings)
|
|
}
|
|
|
|
func (env *testEnv) initPodCache(pod *v1.Pod, node string, bindings []*bindingInfo, provisionings []*v1.PersistentVolumeClaim) {
|
|
cache := env.internalBinder.podBindingCache
|
|
cache.UpdateBindings(pod, node, bindings)
|
|
|
|
cache.UpdateProvisionedPVCs(pod, node, provisionings)
|
|
}
|
|
|
|
func (env *testEnv) validatePodCache(t *testing.T, name, node string, pod *v1.Pod, expectedBindings []*bindingInfo, expectedProvisionings []*v1.PersistentVolumeClaim) {
|
|
cache := env.internalBinder.podBindingCache
|
|
bindings := cache.GetBindings(pod, node)
|
|
if aLen, eLen := len(bindings), len(expectedBindings); aLen != eLen {
|
|
t.Errorf("Test %q failed. expected %v bindings, got %v", name, eLen, aLen)
|
|
} else if expectedBindings == nil && bindings != nil {
|
|
// nil and empty are different
|
|
t.Errorf("Test %q failed. expected nil bindings, got empty", name)
|
|
} else if expectedBindings != nil && bindings == nil {
|
|
// nil and empty are different
|
|
t.Errorf("Test %q failed. expected empty bindings, got nil", name)
|
|
} else {
|
|
for i := 0; i < aLen; i++ {
|
|
// Validate PV
|
|
if !reflect.DeepEqual(expectedBindings[i].pv, bindings[i].pv) {
|
|
t.Errorf("Test %q failed. binding.pv doesn't match [A-expected, B-got]: %s", name, diff.ObjectDiff(expectedBindings[i].pv, bindings[i].pv))
|
|
}
|
|
|
|
// Validate PVC
|
|
if !reflect.DeepEqual(expectedBindings[i].pvc, bindings[i].pvc) {
|
|
t.Errorf("Test %q failed. binding.pvc doesn't match [A-expected, B-got]: %s", name, diff.ObjectDiff(expectedBindings[i].pvc, bindings[i].pvc))
|
|
}
|
|
}
|
|
}
|
|
|
|
provisionedClaims := cache.GetProvisionedPVCs(pod, node)
|
|
if aLen, eLen := len(provisionedClaims), len(expectedProvisionings); aLen != eLen {
|
|
t.Errorf("Test %q failed. expected %v provisioned claims, got %v", name, eLen, aLen)
|
|
} else if expectedProvisionings == nil && provisionedClaims != nil {
|
|
// nil and empty are different
|
|
t.Errorf("Test %q failed. expected nil provisionings, got empty", name)
|
|
} else if expectedProvisionings != nil && provisionedClaims == nil {
|
|
// nil and empty are different
|
|
t.Errorf("Test %q failed. expected empty provisionings, got nil", name)
|
|
} else {
|
|
for i := 0; i < aLen; i++ {
|
|
if !reflect.DeepEqual(expectedProvisionings[i], provisionedClaims[i]) {
|
|
t.Errorf("Test %q failed. provisioned claims doesn't match [A-expected, B-got]: %s", name, diff.ObjectDiff(expectedProvisionings[i], provisionedClaims[i]))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (env *testEnv) getPodBindings(t *testing.T, name, node string, pod *v1.Pod) []*bindingInfo {
|
|
cache := env.internalBinder.podBindingCache
|
|
return cache.GetBindings(pod, node)
|
|
}
|
|
|
|
func (env *testEnv) validateAssume(t *testing.T, name string, pod *v1.Pod, bindings []*bindingInfo, provisionings []*v1.PersistentVolumeClaim) {
|
|
// Check pv cache
|
|
pvCache := env.internalBinder.pvCache
|
|
for _, b := range bindings {
|
|
pv, err := pvCache.GetPV(b.pv.Name)
|
|
if err != nil {
|
|
t.Errorf("Test %q failed: GetPV %q returned error: %v", name, b.pv.Name, err)
|
|
continue
|
|
}
|
|
if pv.Spec.ClaimRef == nil {
|
|
t.Errorf("Test %q failed: PV %q ClaimRef is nil", name, b.pv.Name)
|
|
continue
|
|
}
|
|
if pv.Spec.ClaimRef.Name != b.pvc.Name {
|
|
t.Errorf("Test %q failed: expected PV.ClaimRef.Name %q, got %q", name, b.pvc.Name, pv.Spec.ClaimRef.Name)
|
|
}
|
|
if pv.Spec.ClaimRef.Namespace != b.pvc.Namespace {
|
|
t.Errorf("Test %q failed: expected PV.ClaimRef.Namespace %q, got %q", name, b.pvc.Namespace, pv.Spec.ClaimRef.Namespace)
|
|
}
|
|
}
|
|
|
|
// Check pvc cache
|
|
pvcCache := env.internalBinder.pvcCache
|
|
for _, p := range provisionings {
|
|
pvcKey := getPVCName(p)
|
|
pvc, err := pvcCache.GetPVC(pvcKey)
|
|
if err != nil {
|
|
t.Errorf("Test %q failed: GetPVC %q returned error: %v", name, pvcKey, err)
|
|
continue
|
|
}
|
|
if pvc.Annotations[annSelectedNode] != nodeLabelValue {
|
|
t.Errorf("Test %q failed: expected annSelectedNode of pvc %q to be %q, but got %q", name, pvcKey, nodeLabelValue, pvc.Annotations[annSelectedNode])
|
|
}
|
|
}
|
|
}
|
|
|
|
func (env *testEnv) validateFailedAssume(t *testing.T, name string, pod *v1.Pod, bindings []*bindingInfo, provisionings []*v1.PersistentVolumeClaim) {
|
|
// All PVs have been unmodified in cache
|
|
pvCache := env.internalBinder.pvCache
|
|
for _, b := range bindings {
|
|
pv, _ := pvCache.GetPV(b.pv.Name)
|
|
// PV could be nil if it's missing from cache
|
|
if pv != nil && pv != b.pv {
|
|
t.Errorf("Test %q failed: PV %q was modified in cache", name, b.pv.Name)
|
|
}
|
|
}
|
|
|
|
// Check pvc cache
|
|
pvcCache := env.internalBinder.pvcCache
|
|
for _, p := range provisionings {
|
|
pvcKey := getPVCName(p)
|
|
pvc, err := pvcCache.GetPVC(pvcKey)
|
|
if err != nil {
|
|
t.Errorf("Test %q failed: GetPVC %q returned error: %v", name, pvcKey, err)
|
|
continue
|
|
}
|
|
if pvc.Annotations[annSelectedNode] != "" {
|
|
t.Errorf("Test %q failed: expected annSelectedNode of pvc %q empty, but got %q", name, pvcKey, pvc.Annotations[annSelectedNode])
|
|
}
|
|
}
|
|
}
|
|
|
|
func (env *testEnv) validateBind(
|
|
t *testing.T,
|
|
name string,
|
|
pod *v1.Pod,
|
|
expectedPVs []*v1.PersistentVolume,
|
|
expectedAPIPVs []*v1.PersistentVolume) {
|
|
|
|
// Check pv cache
|
|
pvCache := env.internalBinder.pvCache
|
|
for _, pv := range expectedPVs {
|
|
cachedPV, err := pvCache.GetPV(pv.Name)
|
|
if err != nil {
|
|
t.Errorf("Test %q failed: GetPV %q returned error: %v", name, pv.Name, err)
|
|
}
|
|
if !reflect.DeepEqual(cachedPV, pv) {
|
|
t.Errorf("Test %q failed: cached PV check failed [A-expected, B-got]:\n%s", name, diff.ObjectDiff(pv, cachedPV))
|
|
}
|
|
}
|
|
|
|
// Check reactor for API updates
|
|
if err := env.reactor.checkVolumes(expectedAPIPVs); err != nil {
|
|
t.Errorf("Test %q failed: API reactor validation failed: %v", name, err)
|
|
}
|
|
}
|
|
|
|
func (env *testEnv) validateProvision(
|
|
t *testing.T,
|
|
name string,
|
|
pod *v1.Pod,
|
|
expectedPVCs []*v1.PersistentVolumeClaim,
|
|
expectedAPIPVCs []*v1.PersistentVolumeClaim) {
|
|
|
|
// Check pvc cache
|
|
pvcCache := env.internalBinder.pvcCache
|
|
for _, pvc := range expectedPVCs {
|
|
cachedPVC, err := pvcCache.GetPVC(getPVCName(pvc))
|
|
if err != nil {
|
|
t.Errorf("Test %q failed: GetPVC %q returned error: %v", name, getPVCName(pvc), err)
|
|
}
|
|
if !reflect.DeepEqual(cachedPVC, pvc) {
|
|
t.Errorf("Test %q failed: cached PVC check failed [A-expected, B-got]:\n%s", name, diff.ObjectDiff(pvc, cachedPVC))
|
|
}
|
|
}
|
|
|
|
// Check reactor for API updates
|
|
if err := env.reactor.checkClaims(expectedAPIPVCs); err != nil {
|
|
t.Errorf("Test %q failed: API reactor validation failed: %v", name, err)
|
|
}
|
|
}
|
|
|
|
const (
|
|
pvcUnbound = iota
|
|
pvcPrebound
|
|
pvcBound
|
|
pvcSelectedNode
|
|
)
|
|
|
|
func makeTestPVC(name, size, node string, pvcBoundState int, pvName, resourceVersion string, className *string) *v1.PersistentVolumeClaim {
|
|
pvc := &v1.PersistentVolumeClaim{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "PersistentVolumeClaim",
|
|
APIVersion: "v1",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: "testns",
|
|
UID: types.UID("pvc-uid"),
|
|
ResourceVersion: resourceVersion,
|
|
SelfLink: testapi.Default.SelfLink("pvc", name),
|
|
},
|
|
Spec: v1.PersistentVolumeClaimSpec{
|
|
Resources: v1.ResourceRequirements{
|
|
Requests: v1.ResourceList{
|
|
v1.ResourceName(v1.ResourceStorage): resource.MustParse(size),
|
|
},
|
|
},
|
|
StorageClassName: className,
|
|
},
|
|
}
|
|
|
|
switch pvcBoundState {
|
|
case pvcSelectedNode:
|
|
metav1.SetMetaDataAnnotation(&pvc.ObjectMeta, annSelectedNode, node)
|
|
// don't fallthrough
|
|
case pvcBound:
|
|
metav1.SetMetaDataAnnotation(&pvc.ObjectMeta, annBindCompleted, "yes")
|
|
fallthrough
|
|
case pvcPrebound:
|
|
pvc.Spec.VolumeName = pvName
|
|
}
|
|
return pvc
|
|
}
|
|
|
|
func makeBadPVC() *v1.PersistentVolumeClaim {
|
|
return &v1.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "bad-pvc",
|
|
Namespace: "testns",
|
|
UID: types.UID("pvc-uid"),
|
|
ResourceVersion: "1",
|
|
// Don't include SefLink, so that GetReference will fail
|
|
},
|
|
Spec: v1.PersistentVolumeClaimSpec{
|
|
Resources: v1.ResourceRequirements{
|
|
Requests: v1.ResourceList{
|
|
v1.ResourceName(v1.ResourceStorage): resource.MustParse("1G"),
|
|
},
|
|
},
|
|
StorageClassName: &waitClass,
|
|
},
|
|
}
|
|
}
|
|
|
|
func makeTestPV(name, node, capacity, version string, boundToPVC *v1.PersistentVolumeClaim, className string) *v1.PersistentVolume {
|
|
pv := &v1.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
ResourceVersion: version,
|
|
},
|
|
Spec: v1.PersistentVolumeSpec{
|
|
Capacity: v1.ResourceList{
|
|
v1.ResourceName(v1.ResourceStorage): resource.MustParse(capacity),
|
|
},
|
|
StorageClassName: className,
|
|
},
|
|
}
|
|
if node != "" {
|
|
pv.Spec.NodeAffinity = getVolumeNodeAffinity(nodeLabelKey, node)
|
|
}
|
|
|
|
if boundToPVC != nil {
|
|
pv.Spec.ClaimRef = &v1.ObjectReference{
|
|
Kind: boundToPVC.Kind,
|
|
APIVersion: boundToPVC.APIVersion,
|
|
ResourceVersion: boundToPVC.ResourceVersion,
|
|
Name: boundToPVC.Name,
|
|
Namespace: boundToPVC.Namespace,
|
|
UID: boundToPVC.UID,
|
|
}
|
|
metav1.SetMetaDataAnnotation(&pv.ObjectMeta, annBoundByController, "yes")
|
|
}
|
|
|
|
return pv
|
|
}
|
|
|
|
func pvcSetSelectedNode(pvc *v1.PersistentVolumeClaim, node string) *v1.PersistentVolumeClaim {
|
|
newPVC := pvc.DeepCopy()
|
|
metav1.SetMetaDataAnnotation(&pvc.ObjectMeta, annSelectedNode, node)
|
|
return newPVC
|
|
}
|
|
|
|
func pvcSetEmptyAnnotations(pvc *v1.PersistentVolumeClaim) *v1.PersistentVolumeClaim {
|
|
newPVC := pvc.DeepCopy()
|
|
newPVC.Annotations = map[string]string{}
|
|
return newPVC
|
|
}
|
|
|
|
func pvRemoveClaimUID(pv *v1.PersistentVolume) *v1.PersistentVolume {
|
|
newPV := pv.DeepCopy()
|
|
newPV.Spec.ClaimRef.UID = ""
|
|
return newPV
|
|
}
|
|
|
|
func makePod(pvcs []*v1.PersistentVolumeClaim) *v1.Pod {
|
|
pod := &v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-pod",
|
|
Namespace: "testns",
|
|
},
|
|
}
|
|
|
|
volumes := []v1.Volume{}
|
|
for i, pvc := range pvcs {
|
|
pvcVol := v1.Volume{
|
|
Name: fmt.Sprintf("vol%v", i),
|
|
VolumeSource: v1.VolumeSource{
|
|
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
|
|
ClaimName: pvc.Name,
|
|
},
|
|
},
|
|
}
|
|
volumes = append(volumes, pvcVol)
|
|
}
|
|
pod.Spec.Volumes = volumes
|
|
pod.Spec.NodeName = "node1"
|
|
return pod
|
|
}
|
|
|
|
func makePodWithoutPVC() *v1.Pod {
|
|
pod := &v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-pod",
|
|
Namespace: "testns",
|
|
},
|
|
Spec: v1.PodSpec{
|
|
Volumes: []v1.Volume{
|
|
{
|
|
VolumeSource: v1.VolumeSource{
|
|
EmptyDir: &v1.EmptyDirVolumeSource{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return pod
|
|
}
|
|
|
|
func makeBinding(pvc *v1.PersistentVolumeClaim, pv *v1.PersistentVolume) *bindingInfo {
|
|
return &bindingInfo{pvc: pvc, pv: pv}
|
|
}
|
|
|
|
func addProvisionAnn(pvc *v1.PersistentVolumeClaim) *v1.PersistentVolumeClaim {
|
|
res := pvc.DeepCopy()
|
|
// Add provision related annotations
|
|
metav1.SetMetaDataAnnotation(&res.ObjectMeta, annSelectedNode, nodeLabelValue)
|
|
|
|
return res
|
|
}
|
|
|
|
func TestFindPodVolumesWithoutProvisioning(t *testing.T) {
|
|
scenarios := map[string]struct {
|
|
// Inputs
|
|
pvs []*v1.PersistentVolume
|
|
podPVCs []*v1.PersistentVolumeClaim
|
|
// If nil, use pod PVCs
|
|
cachePVCs []*v1.PersistentVolumeClaim
|
|
// If nil, makePod with podPVCs
|
|
pod *v1.Pod
|
|
|
|
// Expected podBindingCache fields
|
|
expectedBindings []*bindingInfo
|
|
|
|
// Expected return values
|
|
expectedUnbound bool
|
|
expectedBound bool
|
|
shouldFail bool
|
|
}{
|
|
"no-volumes": {
|
|
pod: makePod(nil),
|
|
expectedUnbound: true,
|
|
expectedBound: true,
|
|
},
|
|
"no-pvcs": {
|
|
pod: makePodWithoutPVC(),
|
|
expectedUnbound: true,
|
|
expectedBound: true,
|
|
},
|
|
"pvc-not-found": {
|
|
cachePVCs: []*v1.PersistentVolumeClaim{},
|
|
podPVCs: []*v1.PersistentVolumeClaim{boundPVC},
|
|
expectedUnbound: false,
|
|
expectedBound: false,
|
|
shouldFail: true,
|
|
},
|
|
"bound-pvc": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{boundPVC},
|
|
pvs: []*v1.PersistentVolume{pvBound},
|
|
expectedUnbound: true,
|
|
expectedBound: true,
|
|
},
|
|
"bound-pvc,pv-not-exists": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{boundPVC},
|
|
expectedUnbound: false,
|
|
expectedBound: false,
|
|
shouldFail: true,
|
|
},
|
|
"prebound-pvc": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{preboundPVC},
|
|
pvs: []*v1.PersistentVolume{pvNode1aBound},
|
|
shouldFail: true,
|
|
},
|
|
"unbound-pvc,pv-same-node": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC},
|
|
pvs: []*v1.PersistentVolume{pvNode2, pvNode1a, pvNode1b},
|
|
expectedBindings: []*bindingInfo{binding1a},
|
|
expectedUnbound: true,
|
|
expectedBound: true,
|
|
},
|
|
"unbound-pvc,pv-different-node": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC},
|
|
pvs: []*v1.PersistentVolume{pvNode2},
|
|
expectedUnbound: false,
|
|
expectedBound: true,
|
|
},
|
|
"two-unbound-pvcs": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, unboundPVC2},
|
|
pvs: []*v1.PersistentVolume{pvNode1a, pvNode1b},
|
|
expectedBindings: []*bindingInfo{binding1a, binding1b},
|
|
expectedUnbound: true,
|
|
expectedBound: true,
|
|
},
|
|
"two-unbound-pvcs,order-by-size": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC2, unboundPVC},
|
|
pvs: []*v1.PersistentVolume{pvNode1a, pvNode1b},
|
|
expectedBindings: []*bindingInfo{binding1a, binding1b},
|
|
expectedUnbound: true,
|
|
expectedBound: true,
|
|
},
|
|
"two-unbound-pvcs,partial-match": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, unboundPVC2},
|
|
pvs: []*v1.PersistentVolume{pvNode1a},
|
|
expectedBindings: []*bindingInfo{binding1a},
|
|
expectedUnbound: false,
|
|
expectedBound: true,
|
|
},
|
|
"one-bound,one-unbound": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, boundPVC},
|
|
pvs: []*v1.PersistentVolume{pvBound, pvNode1a},
|
|
expectedBindings: []*bindingInfo{binding1a},
|
|
expectedUnbound: true,
|
|
expectedBound: true,
|
|
},
|
|
"one-bound,one-unbound,no-match": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, boundPVC},
|
|
pvs: []*v1.PersistentVolume{pvBound, pvNode2},
|
|
expectedUnbound: false,
|
|
expectedBound: true,
|
|
},
|
|
"one-prebound,one-unbound": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, preboundPVC},
|
|
pvs: []*v1.PersistentVolume{pvNode1a, pvNode1b},
|
|
shouldFail: true,
|
|
},
|
|
"immediate-bound-pvc": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{immediateBoundPVC},
|
|
pvs: []*v1.PersistentVolume{pvBoundImmediate},
|
|
expectedUnbound: true,
|
|
expectedBound: true,
|
|
},
|
|
"immediate-bound-pvc-wrong-node": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{immediateBoundPVC},
|
|
pvs: []*v1.PersistentVolume{pvBoundImmediateNode2},
|
|
expectedUnbound: true,
|
|
expectedBound: false,
|
|
},
|
|
"immediate-unbound-pvc": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{immediateUnboundPVC},
|
|
expectedUnbound: false,
|
|
expectedBound: false,
|
|
shouldFail: true,
|
|
},
|
|
"immediate-unbound-pvc,delayed-mode-bound": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{immediateUnboundPVC, boundPVC},
|
|
pvs: []*v1.PersistentVolume{pvBound},
|
|
expectedUnbound: false,
|
|
expectedBound: false,
|
|
shouldFail: true,
|
|
},
|
|
"immediate-unbound-pvc,delayed-mode-unbound": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{immediateUnboundPVC, unboundPVC},
|
|
expectedUnbound: false,
|
|
expectedBound: false,
|
|
shouldFail: true,
|
|
},
|
|
}
|
|
|
|
// Set feature gate
|
|
utilfeature.DefaultFeatureGate.Set("VolumeScheduling=true")
|
|
defer utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false")
|
|
|
|
testNode := &v1.Node{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "node1",
|
|
Labels: map[string]string{
|
|
nodeLabelKey: "node1",
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, scenario := range scenarios {
|
|
glog.V(5).Infof("Running test case %q", name)
|
|
|
|
// Setup
|
|
testEnv := newTestBinder(t)
|
|
testEnv.initVolumes(scenario.pvs, scenario.pvs)
|
|
|
|
// a. Init pvc cache
|
|
if scenario.cachePVCs == nil {
|
|
scenario.cachePVCs = scenario.podPVCs
|
|
}
|
|
testEnv.initClaims(scenario.cachePVCs, scenario.cachePVCs)
|
|
|
|
// b. Generate pod with given claims
|
|
if scenario.pod == nil {
|
|
scenario.pod = makePod(scenario.podPVCs)
|
|
}
|
|
|
|
// Execute
|
|
unboundSatisfied, boundSatisfied, err := testEnv.binder.FindPodVolumes(scenario.pod, testNode)
|
|
|
|
// Validate
|
|
if !scenario.shouldFail && err != nil {
|
|
t.Errorf("Test %q failed: returned error: %v", name, err)
|
|
}
|
|
if scenario.shouldFail && err == nil {
|
|
t.Errorf("Test %q failed: returned success but expected error", name)
|
|
}
|
|
if boundSatisfied != scenario.expectedBound {
|
|
t.Errorf("Test %q failed: expected boundSatsified %v, got %v", name, scenario.expectedBound, boundSatisfied)
|
|
}
|
|
if unboundSatisfied != scenario.expectedUnbound {
|
|
t.Errorf("Test %q failed: expected unboundSatsified %v, got %v", name, scenario.expectedUnbound, unboundSatisfied)
|
|
}
|
|
testEnv.validatePodCache(t, name, testNode.Name, scenario.pod, scenario.expectedBindings, nil)
|
|
}
|
|
}
|
|
|
|
func TestFindPodVolumesWithProvisioning(t *testing.T) {
|
|
scenarios := map[string]struct {
|
|
// Inputs
|
|
pvs []*v1.PersistentVolume
|
|
podPVCs []*v1.PersistentVolumeClaim
|
|
// If nil, use pod PVCs
|
|
cachePVCs []*v1.PersistentVolumeClaim
|
|
// If nil, makePod with podPVCs
|
|
pod *v1.Pod
|
|
|
|
// Expected podBindingCache fields
|
|
expectedBindings []*bindingInfo
|
|
expectedProvisions []*v1.PersistentVolumeClaim
|
|
|
|
// Expected return values
|
|
expectedUnbound bool
|
|
expectedBound bool
|
|
shouldFail bool
|
|
}{
|
|
"one-provisioned": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
|
expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC},
|
|
expectedUnbound: true,
|
|
expectedBound: true,
|
|
},
|
|
"two-unbound-pvcs,one-matched,one-provisioned": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, provisionedPVC},
|
|
pvs: []*v1.PersistentVolume{pvNode1a},
|
|
expectedBindings: []*bindingInfo{binding1a},
|
|
expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC},
|
|
expectedUnbound: true,
|
|
expectedBound: true,
|
|
},
|
|
"one-bound,one-provisioned": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{boundPVC, provisionedPVC},
|
|
pvs: []*v1.PersistentVolume{pvBound},
|
|
expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC},
|
|
expectedUnbound: true,
|
|
expectedBound: true,
|
|
},
|
|
"immediate-unbound-pvc": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{immediateUnboundPVC},
|
|
expectedUnbound: false,
|
|
expectedBound: false,
|
|
shouldFail: true,
|
|
},
|
|
"one-immediate-bound,one-provisioned": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{immediateBoundPVC, provisionedPVC},
|
|
pvs: []*v1.PersistentVolume{pvBoundImmediate},
|
|
expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC},
|
|
expectedUnbound: true,
|
|
expectedBound: true,
|
|
},
|
|
"invalid-provisioner": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{noProvisionerPVC},
|
|
expectedUnbound: false,
|
|
expectedBound: true,
|
|
},
|
|
"volume-topology-unsatisfied": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{topoMismatchPVC},
|
|
expectedUnbound: false,
|
|
expectedBound: true,
|
|
},
|
|
}
|
|
|
|
// Set VolumeScheduling feature gate
|
|
utilfeature.DefaultFeatureGate.Set("VolumeScheduling=true")
|
|
defer utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false")
|
|
|
|
testNode := &v1.Node{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "node1",
|
|
Labels: map[string]string{
|
|
nodeLabelKey: "node1",
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, scenario := range scenarios {
|
|
// Setup
|
|
testEnv := newTestBinder(t)
|
|
testEnv.initVolumes(scenario.pvs, scenario.pvs)
|
|
|
|
// a. Init pvc cache
|
|
if scenario.cachePVCs == nil {
|
|
scenario.cachePVCs = scenario.podPVCs
|
|
}
|
|
testEnv.initClaims(scenario.cachePVCs, scenario.cachePVCs)
|
|
|
|
// b. Generate pod with given claims
|
|
if scenario.pod == nil {
|
|
scenario.pod = makePod(scenario.podPVCs)
|
|
}
|
|
|
|
// Execute
|
|
unboundSatisfied, boundSatisfied, err := testEnv.binder.FindPodVolumes(scenario.pod, testNode)
|
|
|
|
// Validate
|
|
if !scenario.shouldFail && err != nil {
|
|
t.Errorf("Test %q failed: returned error: %v", name, err)
|
|
}
|
|
if scenario.shouldFail && err == nil {
|
|
t.Errorf("Test %q failed: returned success but expected error", name)
|
|
}
|
|
if boundSatisfied != scenario.expectedBound {
|
|
t.Errorf("Test %q failed: expected boundSatsified %v, got %v", name, scenario.expectedBound, boundSatisfied)
|
|
}
|
|
if unboundSatisfied != scenario.expectedUnbound {
|
|
t.Errorf("Test %q failed: expected unboundSatsified %v, got %v", name, scenario.expectedUnbound, unboundSatisfied)
|
|
}
|
|
testEnv.validatePodCache(t, name, testNode.Name, scenario.pod, scenario.expectedBindings, scenario.expectedProvisions)
|
|
}
|
|
}
|
|
|
|
func TestAssumePodVolumes(t *testing.T) {
|
|
scenarios := map[string]struct {
|
|
// Inputs
|
|
podPVCs []*v1.PersistentVolumeClaim
|
|
pvs []*v1.PersistentVolume
|
|
bindings []*bindingInfo
|
|
provisionedPVCs []*v1.PersistentVolumeClaim
|
|
|
|
// Expected return values
|
|
shouldFail bool
|
|
expectedAllBound bool
|
|
|
|
expectedBindings []*bindingInfo
|
|
expectedProvisionings []*v1.PersistentVolumeClaim
|
|
}{
|
|
"all-bound": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{boundPVC},
|
|
pvs: []*v1.PersistentVolume{pvBound},
|
|
expectedAllBound: true,
|
|
},
|
|
"one-binding": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC},
|
|
bindings: []*bindingInfo{binding1a},
|
|
pvs: []*v1.PersistentVolume{pvNode1a},
|
|
expectedBindings: []*bindingInfo{binding1aBound},
|
|
expectedProvisionings: []*v1.PersistentVolumeClaim{},
|
|
},
|
|
"two-bindings": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, unboundPVC2},
|
|
bindings: []*bindingInfo{binding1a, binding1b},
|
|
pvs: []*v1.PersistentVolume{pvNode1a, pvNode1b},
|
|
expectedBindings: []*bindingInfo{binding1aBound, binding1bBound},
|
|
expectedProvisionings: []*v1.PersistentVolumeClaim{},
|
|
},
|
|
"pv-already-bound": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC},
|
|
bindings: []*bindingInfo{binding1aBound},
|
|
pvs: []*v1.PersistentVolume{pvNode1aBound},
|
|
expectedBindings: []*bindingInfo{binding1aBound},
|
|
expectedProvisionings: []*v1.PersistentVolumeClaim{},
|
|
},
|
|
"claimref-failed": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC},
|
|
bindings: []*bindingInfo{binding1a, bindingBad},
|
|
pvs: []*v1.PersistentVolume{pvNode1a, pvNode1b},
|
|
shouldFail: true,
|
|
},
|
|
"tmpupdate-failed": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC},
|
|
bindings: []*bindingInfo{binding1a, binding1b},
|
|
pvs: []*v1.PersistentVolume{pvNode1a},
|
|
shouldFail: true,
|
|
},
|
|
"one-binding, one-pvc-provisioned": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, provisionedPVC},
|
|
bindings: []*bindingInfo{binding1a},
|
|
pvs: []*v1.PersistentVolume{pvNode1a},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
|
expectedBindings: []*bindingInfo{binding1aBound},
|
|
expectedProvisionings: []*v1.PersistentVolumeClaim{selectedNodePVC},
|
|
},
|
|
"one-binding, one-provision-tmpupdate-failed": {
|
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, provisionedPVCHigherVersion},
|
|
bindings: []*bindingInfo{binding1a},
|
|
pvs: []*v1.PersistentVolume{pvNode1a},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{provisionedPVC2},
|
|
shouldFail: true,
|
|
},
|
|
}
|
|
|
|
for name, scenario := range scenarios {
|
|
glog.V(5).Infof("Running test case %q", name)
|
|
|
|
// Setup
|
|
testEnv := newTestBinder(t)
|
|
testEnv.initClaims(scenario.podPVCs, scenario.podPVCs)
|
|
pod := makePod(scenario.podPVCs)
|
|
testEnv.initPodCache(pod, "node1", scenario.bindings, scenario.provisionedPVCs)
|
|
testEnv.initVolumes(scenario.pvs, scenario.pvs)
|
|
|
|
// Execute
|
|
allBound, err := testEnv.binder.AssumePodVolumes(pod, "node1")
|
|
|
|
// Validate
|
|
if !scenario.shouldFail && err != nil {
|
|
t.Errorf("Test %q failed: returned error: %v", name, err)
|
|
}
|
|
if scenario.shouldFail && err == nil {
|
|
t.Errorf("Test %q failed: returned success but expected error", name)
|
|
}
|
|
if scenario.expectedAllBound != allBound {
|
|
t.Errorf("Test %q failed: returned unexpected allBound: %v", name, allBound)
|
|
}
|
|
if scenario.expectedBindings == nil {
|
|
scenario.expectedBindings = scenario.bindings
|
|
}
|
|
if scenario.expectedProvisionings == nil {
|
|
scenario.expectedProvisionings = scenario.provisionedPVCs
|
|
}
|
|
if scenario.shouldFail {
|
|
testEnv.validateFailedAssume(t, name, pod, scenario.expectedBindings, scenario.expectedProvisionings)
|
|
} else {
|
|
testEnv.validateAssume(t, name, pod, scenario.expectedBindings, scenario.expectedProvisionings)
|
|
}
|
|
testEnv.validatePodCache(t, name, pod.Spec.NodeName, pod, scenario.expectedBindings, scenario.expectedProvisionings)
|
|
}
|
|
}
|
|
|
|
func TestBindAPIUpdate(t *testing.T) {
|
|
scenarios := map[string]struct {
|
|
// Inputs
|
|
bindings []*bindingInfo
|
|
cachedPVs []*v1.PersistentVolume
|
|
// if nil, use cachedPVs
|
|
apiPVs []*v1.PersistentVolume
|
|
|
|
provisionedPVCs []*v1.PersistentVolumeClaim
|
|
cachedPVCs []*v1.PersistentVolumeClaim
|
|
// if nil, use cachedPVCs
|
|
apiPVCs []*v1.PersistentVolumeClaim
|
|
|
|
// Expected return values
|
|
shouldFail bool
|
|
expectedPVs []*v1.PersistentVolume
|
|
// if nil, use expectedPVs
|
|
expectedAPIPVs []*v1.PersistentVolume
|
|
|
|
expectedPVCs []*v1.PersistentVolumeClaim
|
|
// if nil, use expectedPVCs
|
|
expectedAPIPVCs []*v1.PersistentVolumeClaim
|
|
}{
|
|
"nothing-to-bind-nil": {
|
|
shouldFail: true,
|
|
},
|
|
"nothing-to-bind-bindings-nil": {
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
shouldFail: true,
|
|
},
|
|
"nothing-to-bind-provisionings-nil": {
|
|
bindings: []*bindingInfo{},
|
|
shouldFail: true,
|
|
},
|
|
"nothing-to-bind-empty": {
|
|
bindings: []*bindingInfo{},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
},
|
|
"one-binding": {
|
|
bindings: []*bindingInfo{binding1aBound},
|
|
cachedPVs: []*v1.PersistentVolume{pvNode1a},
|
|
expectedPVs: []*v1.PersistentVolume{pvNode1aBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
},
|
|
"two-bindings": {
|
|
bindings: []*bindingInfo{binding1aBound, binding1bBound},
|
|
cachedPVs: []*v1.PersistentVolume{pvNode1a, pvNode1b},
|
|
expectedPVs: []*v1.PersistentVolume{pvNode1aBound, pvNode1bBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
},
|
|
"api-already-updated": {
|
|
bindings: []*bindingInfo{binding1aBound},
|
|
cachedPVs: []*v1.PersistentVolume{pvNode1aBound},
|
|
expectedPVs: []*v1.PersistentVolume{pvNode1aBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
},
|
|
"api-update-failed": {
|
|
bindings: []*bindingInfo{binding1aBound, binding1bBound},
|
|
cachedPVs: []*v1.PersistentVolume{pvNode1a, pvNode1b},
|
|
apiPVs: []*v1.PersistentVolume{pvNode1a, pvNode1bBoundHigherVersion},
|
|
expectedPVs: []*v1.PersistentVolume{pvNode1aBound, pvNode1b},
|
|
expectedAPIPVs: []*v1.PersistentVolume{pvNode1aBound, pvNode1bBoundHigherVersion},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
shouldFail: true,
|
|
},
|
|
"one-provisioned-pvc": {
|
|
bindings: []*bindingInfo{},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
|
expectedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)},
|
|
},
|
|
"provision-api-update-failed": {
|
|
bindings: []*bindingInfo{},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC), addProvisionAnn(provisionedPVC2)},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{provisionedPVC, provisionedPVC2},
|
|
apiPVCs: []*v1.PersistentVolumeClaim{provisionedPVC, provisionedPVCHigherVersion},
|
|
expectedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC), provisionedPVC2},
|
|
expectedAPIPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC), provisionedPVCHigherVersion},
|
|
shouldFail: true,
|
|
},
|
|
"binding-succeed, provision-api-update-failed": {
|
|
bindings: []*bindingInfo{binding1aBound},
|
|
cachedPVs: []*v1.PersistentVolume{pvNode1a},
|
|
expectedPVs: []*v1.PersistentVolume{pvNode1aBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC), addProvisionAnn(provisionedPVC2)},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{provisionedPVC, provisionedPVC2},
|
|
apiPVCs: []*v1.PersistentVolumeClaim{provisionedPVC, provisionedPVCHigherVersion},
|
|
expectedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC), provisionedPVC2},
|
|
expectedAPIPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC), provisionedPVCHigherVersion},
|
|
shouldFail: true,
|
|
},
|
|
}
|
|
for name, scenario := range scenarios {
|
|
glog.V(4).Infof("Running test case %q", name)
|
|
|
|
// Setup
|
|
testEnv := newTestBinder(t)
|
|
pod := makePod(nil)
|
|
if scenario.apiPVs == nil {
|
|
scenario.apiPVs = scenario.cachedPVs
|
|
}
|
|
if scenario.apiPVCs == nil {
|
|
scenario.apiPVCs = scenario.cachedPVCs
|
|
}
|
|
testEnv.initVolumes(scenario.cachedPVs, scenario.apiPVs)
|
|
testEnv.initClaims(scenario.cachedPVCs, scenario.apiPVCs)
|
|
testEnv.assumeVolumes(t, name, "node1", pod, scenario.bindings, scenario.provisionedPVCs)
|
|
|
|
// Execute
|
|
err := testEnv.internalBinder.bindAPIUpdate(pod.Name, scenario.bindings, scenario.provisionedPVCs)
|
|
|
|
// Validate
|
|
if !scenario.shouldFail && err != nil {
|
|
t.Errorf("Test %q failed: returned error: %v", name, err)
|
|
}
|
|
if scenario.shouldFail && err == nil {
|
|
t.Errorf("Test %q failed: returned success but expected error", name)
|
|
}
|
|
if scenario.expectedAPIPVs == nil {
|
|
scenario.expectedAPIPVs = scenario.expectedPVs
|
|
}
|
|
if scenario.expectedAPIPVCs == nil {
|
|
scenario.expectedAPIPVCs = scenario.expectedPVCs
|
|
}
|
|
testEnv.validateBind(t, name, pod, scenario.expectedPVs, scenario.expectedAPIPVs)
|
|
testEnv.validateProvision(t, name, pod, scenario.expectedPVCs, scenario.expectedAPIPVCs)
|
|
}
|
|
}
|
|
|
|
func TestCheckBindings(t *testing.T) {
|
|
scenarios := map[string]struct {
|
|
// Inputs
|
|
bindings []*bindingInfo
|
|
cachedPVs []*v1.PersistentVolume
|
|
|
|
provisionedPVCs []*v1.PersistentVolumeClaim
|
|
cachedPVCs []*v1.PersistentVolumeClaim
|
|
|
|
// Expected return values
|
|
shouldFail bool
|
|
expectedBound bool
|
|
}{
|
|
"nothing-to-bind-nil": {
|
|
shouldFail: true,
|
|
},
|
|
"nothing-to-bind-bindings-nil": {
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
shouldFail: true,
|
|
},
|
|
"nothing-to-bind-provisionings-nil": {
|
|
bindings: []*bindingInfo{},
|
|
shouldFail: true,
|
|
},
|
|
"nothing-to-bind": {
|
|
bindings: []*bindingInfo{},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
expectedBound: true,
|
|
},
|
|
"binding-bound": {
|
|
bindings: []*bindingInfo{binding1aBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
cachedPVs: []*v1.PersistentVolume{pvNode1aBound},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a},
|
|
expectedBound: true,
|
|
},
|
|
"binding-prebound": {
|
|
bindings: []*bindingInfo{binding1aBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
cachedPVs: []*v1.PersistentVolume{pvNode1aBound},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{preboundPVCNode1a},
|
|
},
|
|
"binding-unbound": {
|
|
bindings: []*bindingInfo{binding1aBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
cachedPVs: []*v1.PersistentVolume{pvNode1aBound},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{unboundPVC},
|
|
},
|
|
"binding-pvc-not-exists": {
|
|
bindings: []*bindingInfo{binding1aBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
cachedPVs: []*v1.PersistentVolume{pvNode1aBound},
|
|
shouldFail: true,
|
|
},
|
|
"binding-pv-not-exists": {
|
|
bindings: []*bindingInfo{binding1aBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a},
|
|
shouldFail: true,
|
|
},
|
|
"binding-claimref-nil": {
|
|
bindings: []*bindingInfo{binding1aBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
cachedPVs: []*v1.PersistentVolume{pvNode1a},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a},
|
|
shouldFail: true,
|
|
},
|
|
"binding-claimref-uid-empty": {
|
|
bindings: []*bindingInfo{binding1aBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
cachedPVs: []*v1.PersistentVolume{pvRemoveClaimUID(pvNode1aBound)},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a},
|
|
shouldFail: true,
|
|
},
|
|
"binding-one-bound,one-unbound": {
|
|
bindings: []*bindingInfo{binding1aBound, binding1bBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{},
|
|
cachedPVs: []*v1.PersistentVolume{pvNode1aBound, pvNode1bBound},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a, unboundPVC2},
|
|
},
|
|
"provisioning-pvc-bound": {
|
|
bindings: []*bindingInfo{},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVCBound)},
|
|
expectedBound: true,
|
|
},
|
|
"provisioning-pvc-unbound": {
|
|
bindings: []*bindingInfo{},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)},
|
|
},
|
|
"provisioning-pvc-not-exists": {
|
|
bindings: []*bindingInfo{},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)},
|
|
shouldFail: true,
|
|
},
|
|
"provisioning-pvc-annotations-nil": {
|
|
bindings: []*bindingInfo{},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
|
shouldFail: true,
|
|
},
|
|
"provisioning-pvc-selected-node-dropped": {
|
|
bindings: []*bindingInfo{},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{pvcSetEmptyAnnotations(provisionedPVC)},
|
|
shouldFail: true,
|
|
},
|
|
"provisioning-pvc-selected-node-wrong-node": {
|
|
bindings: []*bindingInfo{},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{pvcSetSelectedNode(provisionedPVC, "wrong-node")},
|
|
shouldFail: true,
|
|
},
|
|
"binding-bound-provisioning-unbound": {
|
|
bindings: []*bindingInfo{binding1aBound},
|
|
provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)},
|
|
cachedPVs: []*v1.PersistentVolume{pvNode1aBound},
|
|
cachedPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a, addProvisionAnn(provisionedPVC)},
|
|
},
|
|
}
|
|
|
|
for name, scenario := range scenarios {
|
|
glog.V(4).Infof("Running test case %q", name)
|
|
|
|
// Setup
|
|
pod := makePod(nil)
|
|
testEnv := newTestBinder(t)
|
|
testEnv.initVolumes(scenario.cachedPVs, nil)
|
|
testEnv.initClaims(scenario.cachedPVCs, nil)
|
|
|
|
// Execute
|
|
allBound, err := testEnv.internalBinder.checkBindings(pod, scenario.bindings, scenario.provisionedPVCs)
|
|
|
|
// Validate
|
|
if !scenario.shouldFail && err != nil {
|
|
t.Errorf("Test %q failed: returned error: %v", name, err)
|
|
}
|
|
if scenario.shouldFail && err == nil {
|
|
t.Errorf("Test %q failed: returned success but expected error", name)
|
|
}
|
|
if scenario.expectedBound != allBound {
|
|
t.Errorf("Test %q failed: returned bound %v", name, allBound)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBindPodVolumes(t *testing.T) {
|
|
scenarios := map[string]struct {
|
|
// Inputs
|
|
// These tests only support a single pv and pvc and static binding
|
|
bindingsNil bool // Pass in nil bindings slice
|
|
binding *bindingInfo
|
|
cachedPV *v1.PersistentVolume
|
|
cachedPVC *v1.PersistentVolumeClaim
|
|
apiPV *v1.PersistentVolume
|
|
|
|
// This function runs with a delay of 5 seconds
|
|
delayFunc func(*testing.T, *testEnv, *v1.Pod, *v1.PersistentVolume, *v1.PersistentVolumeClaim)
|
|
|
|
// Expected return values
|
|
shouldFail bool
|
|
}{
|
|
"nothing-to-bind-nil": {
|
|
bindingsNil: true,
|
|
shouldFail: true,
|
|
},
|
|
"nothing-to-bind-empty": {},
|
|
"already-bound": {
|
|
binding: binding1aBound,
|
|
cachedPV: pvNode1aBound,
|
|
cachedPVC: boundPVCNode1a,
|
|
},
|
|
"binding-succeeds-after-time": {
|
|
binding: binding1aBound,
|
|
cachedPV: pvNode1a,
|
|
cachedPVC: unboundPVC,
|
|
delayFunc: func(t *testing.T, testEnv *testEnv, pod *v1.Pod, pv *v1.PersistentVolume, pvc *v1.PersistentVolumeClaim) {
|
|
// Update PVC to be fully bound to PV
|
|
newPVC := pvc.DeepCopy()
|
|
newPVC.ResourceVersion = "100"
|
|
newPVC.Spec.VolumeName = pv.Name
|
|
metav1.SetMetaDataAnnotation(&newPVC.ObjectMeta, annBindCompleted, "yes")
|
|
|
|
// Update pvc cache, fake client doesn't invoke informers
|
|
internalBinder, ok := testEnv.binder.(*volumeBinder)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert to internal binder")
|
|
}
|
|
|
|
pvcCache := internalBinder.pvcCache
|
|
internalPVCCache, ok := pvcCache.(*pvcAssumeCache)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert to internal PVC cache")
|
|
}
|
|
internalPVCCache.add(newPVC)
|
|
},
|
|
},
|
|
"pod-deleted-after-time": {
|
|
binding: binding1aBound,
|
|
cachedPV: pvNode1a,
|
|
cachedPVC: unboundPVC,
|
|
delayFunc: func(t *testing.T, testEnv *testEnv, pod *v1.Pod, pv *v1.PersistentVolume, pvc *v1.PersistentVolumeClaim) {
|
|
bindingsCache := testEnv.binder.GetBindingsCache()
|
|
if bindingsCache == nil {
|
|
t.Fatalf("Failed to get bindings cache")
|
|
}
|
|
|
|
// Delete the pod from the cache
|
|
bindingsCache.DeleteBindings(pod)
|
|
|
|
// Check that it's deleted
|
|
bindings := bindingsCache.GetBindings(pod, "node1")
|
|
if bindings != nil {
|
|
t.Fatalf("Failed to delete bindings")
|
|
}
|
|
},
|
|
shouldFail: true,
|
|
},
|
|
"binding-times-out": {
|
|
binding: binding1aBound,
|
|
cachedPV: pvNode1a,
|
|
cachedPVC: unboundPVC,
|
|
shouldFail: true,
|
|
},
|
|
"binding-fails": {
|
|
binding: binding1bBound,
|
|
cachedPV: pvNode1b,
|
|
apiPV: pvNode1bBoundHigherVersion,
|
|
cachedPVC: unboundPVC2,
|
|
shouldFail: true,
|
|
},
|
|
"check-fails": {
|
|
binding: binding1aBound,
|
|
cachedPV: pvNode1a,
|
|
cachedPVC: unboundPVC,
|
|
delayFunc: func(t *testing.T, testEnv *testEnv, pod *v1.Pod, pv *v1.PersistentVolume, pvc *v1.PersistentVolumeClaim) {
|
|
// Delete PVC
|
|
// Update pvc cache, fake client doesn't invoke informers
|
|
internalBinder, ok := testEnv.binder.(*volumeBinder)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert to internal binder")
|
|
}
|
|
|
|
pvcCache := internalBinder.pvcCache
|
|
internalPVCCache, ok := pvcCache.(*pvcAssumeCache)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert to internal PVC cache")
|
|
}
|
|
internalPVCCache.delete(pvc)
|
|
},
|
|
shouldFail: true,
|
|
},
|
|
}
|
|
|
|
for name, scenario := range scenarios {
|
|
glog.V(4).Infof("Running test case %q", name)
|
|
|
|
// Setup
|
|
pod := makePod(nil)
|
|
if scenario.apiPV == nil {
|
|
scenario.apiPV = scenario.cachedPV
|
|
}
|
|
testEnv := newTestBinder(t)
|
|
if !scenario.bindingsNil {
|
|
if scenario.binding != nil {
|
|
testEnv.initVolumes([]*v1.PersistentVolume{scenario.cachedPV}, []*v1.PersistentVolume{scenario.apiPV})
|
|
testEnv.initClaims([]*v1.PersistentVolumeClaim{scenario.cachedPVC}, nil)
|
|
testEnv.assumeVolumes(t, name, "node1", pod, []*bindingInfo{scenario.binding}, []*v1.PersistentVolumeClaim{})
|
|
} else {
|
|
testEnv.assumeVolumes(t, name, "node1", pod, []*bindingInfo{}, []*v1.PersistentVolumeClaim{})
|
|
}
|
|
}
|
|
|
|
if scenario.delayFunc != nil {
|
|
go func() {
|
|
time.Sleep(5 * time.Second)
|
|
glog.V(5).Infof("Running delay function")
|
|
scenario.delayFunc(t, testEnv, pod, scenario.binding.pv, scenario.binding.pvc)
|
|
}()
|
|
}
|
|
|
|
// Execute
|
|
err := testEnv.binder.BindPodVolumes(pod)
|
|
|
|
// Validate
|
|
if !scenario.shouldFail && err != nil {
|
|
t.Errorf("Test %q failed: returned error: %v", name, err)
|
|
}
|
|
if scenario.shouldFail && err == nil {
|
|
t.Errorf("Test %q failed: returned success but expected error", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFindAssumeVolumes(t *testing.T) {
|
|
// Set feature gate
|
|
utilfeature.DefaultFeatureGate.Set("VolumeScheduling=true")
|
|
defer utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false")
|
|
|
|
// Test case
|
|
podPVCs := []*v1.PersistentVolumeClaim{unboundPVC}
|
|
pvs := []*v1.PersistentVolume{pvNode2, pvNode1a, pvNode1c}
|
|
|
|
// Setup
|
|
testEnv := newTestBinder(t)
|
|
testEnv.initVolumes(pvs, pvs)
|
|
testEnv.initClaims(podPVCs, podPVCs)
|
|
pod := makePod(podPVCs)
|
|
|
|
testNode := &v1.Node{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "node1",
|
|
Labels: map[string]string{
|
|
nodeLabelKey: "node1",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Execute
|
|
// 1. Find matching PVs
|
|
unboundSatisfied, _, err := testEnv.binder.FindPodVolumes(pod, testNode)
|
|
if err != nil {
|
|
t.Errorf("Test failed: FindPodVolumes returned error: %v", err)
|
|
}
|
|
if !unboundSatisfied {
|
|
t.Errorf("Test failed: couldn't find PVs for all PVCs")
|
|
}
|
|
expectedBindings := testEnv.getPodBindings(t, "before-assume", testNode.Name, pod)
|
|
|
|
// 2. Assume matches
|
|
allBound, err := testEnv.binder.AssumePodVolumes(pod, testNode.Name)
|
|
if err != nil {
|
|
t.Errorf("Test failed: AssumePodVolumes returned error: %v", err)
|
|
}
|
|
if allBound {
|
|
t.Errorf("Test failed: detected unbound volumes as bound")
|
|
}
|
|
testEnv.validateAssume(t, "assume", pod, expectedBindings, nil)
|
|
|
|
// After assume, claimref should be set on pv
|
|
expectedBindings = testEnv.getPodBindings(t, "after-assume", testNode.Name, pod)
|
|
|
|
// 3. Find matching PVs again
|
|
// This should always return the original chosen pv
|
|
// Run this many times in case sorting returns different orders for the two PVs.
|
|
t.Logf("Testing FindPodVolumes after Assume")
|
|
for i := 0; i < 50; i++ {
|
|
unboundSatisfied, _, err := testEnv.binder.FindPodVolumes(pod, testNode)
|
|
if err != nil {
|
|
t.Errorf("Test failed: FindPodVolumes returned error: %v", err)
|
|
}
|
|
if !unboundSatisfied {
|
|
t.Errorf("Test failed: couldn't find PVs for all PVCs")
|
|
}
|
|
testEnv.validatePodCache(t, "after-assume", testNode.Name, pod, expectedBindings, []*v1.PersistentVolumeClaim{})
|
|
}
|
|
}
|