mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 22:46:12 +00:00
CSIStorageCapacity: check for sufficient storage in volume binder
This uses the information provided by a CSI driver deployment for
checking whether a node has access to enough storage to create the
currently unbound volumes, if the CSI driver opts into that checking
with CSIDriver.Spec.VolumeCapacity != false.
This resolves a TODO from commit 95b530366a
.
This commit is contained in:
parent
9a66e8e1b5
commit
0efbbe8555
@ -17,6 +17,7 @@ go_library(
|
|||||||
"//pkg/volume/util:go_default_library",
|
"//pkg/volume/util:go_default_library",
|
||||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/storage/v1:go_default_library",
|
"//staging/src/k8s.io/api/storage/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/api/storage/v1alpha1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
@ -27,9 +28,11 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/informers/core/v1:go_default_library",
|
"//staging/src/k8s.io/client-go/informers/core/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/informers/storage/v1:go_default_library",
|
"//staging/src/k8s.io/client-go/informers/storage/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/client-go/informers/storage/v1alpha1:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
|
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/listers/storage/v1:go_default_library",
|
"//staging/src/k8s.io/client-go/listers/storage/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/client-go/listers/storage/v1alpha1:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
|
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
|
||||||
"//staging/src/k8s.io/csi-translation-lib:go_default_library",
|
"//staging/src/k8s.io/csi-translation-lib:go_default_library",
|
||||||
"//staging/src/k8s.io/csi-translation-lib/plugins:go_default_library",
|
"//staging/src/k8s.io/csi-translation-lib/plugins:go_default_library",
|
||||||
@ -51,6 +54,7 @@ go_test(
|
|||||||
"//pkg/features:go_default_library",
|
"//pkg/features:go_default_library",
|
||||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/storage/v1:go_default_library",
|
"//staging/src/k8s.io/api/storage/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/api/storage/v1alpha1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||||
@ -61,6 +65,7 @@ go_test(
|
|||||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/informers/core/v1:go_default_library",
|
"//staging/src/k8s.io/client-go/informers/core/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/informers/storage/v1:go_default_library",
|
"//staging/src/k8s.io/client-go/informers/storage/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/client-go/informers/storage/v1alpha1:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/testing:go_default_library",
|
"//staging/src/k8s.io/client-go/testing:go_default_library",
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
storagev1 "k8s.io/api/storage/v1"
|
storagev1 "k8s.io/api/storage/v1"
|
||||||
|
storagev1alpha1 "k8s.io/api/storage/v1alpha1"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
@ -34,9 +35,11 @@ import (
|
|||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
coreinformers "k8s.io/client-go/informers/core/v1"
|
coreinformers "k8s.io/client-go/informers/core/v1"
|
||||||
storageinformers "k8s.io/client-go/informers/storage/v1"
|
storageinformers "k8s.io/client-go/informers/storage/v1"
|
||||||
|
storageinformersv1alpha1 "k8s.io/client-go/informers/storage/v1alpha1"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
corelisters "k8s.io/client-go/listers/core/v1"
|
corelisters "k8s.io/client-go/listers/core/v1"
|
||||||
storagelisters "k8s.io/client-go/listers/storage/v1"
|
storagelisters "k8s.io/client-go/listers/storage/v1"
|
||||||
|
storagelistersv1alpha1 "k8s.io/client-go/listers/storage/v1alpha1"
|
||||||
csitrans "k8s.io/csi-translation-lib"
|
csitrans "k8s.io/csi-translation-lib"
|
||||||
csiplugins "k8s.io/csi-translation-lib/plugins"
|
csiplugins "k8s.io/csi-translation-lib/plugins"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
@ -63,6 +66,8 @@ const (
|
|||||||
ErrReasonBindConflict ConflictReason = "node(s) didn't find available persistent volumes to bind"
|
ErrReasonBindConflict ConflictReason = "node(s) didn't find available persistent volumes to bind"
|
||||||
// ErrReasonNodeConflict is used for VolumeNodeAffinityConflict predicate error.
|
// ErrReasonNodeConflict is used for VolumeNodeAffinityConflict predicate error.
|
||||||
ErrReasonNodeConflict ConflictReason = "node(s) had volume node affinity conflict"
|
ErrReasonNodeConflict ConflictReason = "node(s) had volume node affinity conflict"
|
||||||
|
// ErrReasonNotEnoughSpace is used when a pod cannot start on a node because not enough storage space is available.
|
||||||
|
ErrReasonNotEnoughSpace = "node(s) did not have enough free storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BindingInfo holds a binding between PV and PVC.
|
// BindingInfo holds a binding between PV and PVC.
|
||||||
@ -131,6 +136,9 @@ type SchedulerVolumeBinder interface {
|
|||||||
// It returns an error when something went wrong or a list of reasons why the node is
|
// It returns an error when something went wrong or a list of reasons why the node is
|
||||||
// (currently) not usable for the pod.
|
// (currently) not usable for the pod.
|
||||||
//
|
//
|
||||||
|
// If the CSIStorageCapacity feature is enabled, then it also checks for sufficient storage
|
||||||
|
// for volumes that still need to be created.
|
||||||
|
//
|
||||||
// This function is called by the scheduler VolumeBinding plugin and can be called in parallel
|
// This function is called by the scheduler VolumeBinding plugin and can be called in parallel
|
||||||
FindPodVolumes(pod *v1.Pod, boundClaims, claimsToBind []*v1.PersistentVolumeClaim, node *v1.Node) (podVolumes *PodVolumes, reasons ConflictReasons, err error)
|
FindPodVolumes(pod *v1.Pod, boundClaims, claimsToBind []*v1.PersistentVolumeClaim, node *v1.Node) (podVolumes *PodVolumes, reasons ConflictReasons, err error)
|
||||||
|
|
||||||
@ -174,9 +182,23 @@ type volumeBinder struct {
|
|||||||
bindTimeout time.Duration
|
bindTimeout time.Duration
|
||||||
|
|
||||||
translator InTreeToCSITranslator
|
translator InTreeToCSITranslator
|
||||||
|
|
||||||
|
capacityCheckEnabled bool
|
||||||
|
csiDriverLister storagelisters.CSIDriverLister
|
||||||
|
csiStorageCapacityLister storagelistersv1alpha1.CSIStorageCapacityLister
|
||||||
|
}
|
||||||
|
|
||||||
|
// CapacityCheck contains additional parameters for NewVolumeBinder that
|
||||||
|
// are only needed when checking volume sizes against available storage
|
||||||
|
// capacity is desired.
|
||||||
|
type CapacityCheck struct {
|
||||||
|
CSIDriverInformer storageinformers.CSIDriverInformer
|
||||||
|
CSIStorageCapacityInformer storageinformersv1alpha1.CSIStorageCapacityInformer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewVolumeBinder sets up all the caches needed for the scheduler to make volume binding decisions.
|
// NewVolumeBinder sets up all the caches needed for the scheduler to make volume binding decisions.
|
||||||
|
//
|
||||||
|
// capacityCheck determines whether storage capacity is checked (CSIStorageCapacity feature).
|
||||||
func NewVolumeBinder(
|
func NewVolumeBinder(
|
||||||
kubeClient clientset.Interface,
|
kubeClient clientset.Interface,
|
||||||
podInformer coreinformers.PodInformer,
|
podInformer coreinformers.PodInformer,
|
||||||
@ -185,8 +207,9 @@ func NewVolumeBinder(
|
|||||||
pvcInformer coreinformers.PersistentVolumeClaimInformer,
|
pvcInformer coreinformers.PersistentVolumeClaimInformer,
|
||||||
pvInformer coreinformers.PersistentVolumeInformer,
|
pvInformer coreinformers.PersistentVolumeInformer,
|
||||||
storageClassInformer storageinformers.StorageClassInformer,
|
storageClassInformer storageinformers.StorageClassInformer,
|
||||||
|
capacityCheck *CapacityCheck,
|
||||||
bindTimeout time.Duration) SchedulerVolumeBinder {
|
bindTimeout time.Duration) SchedulerVolumeBinder {
|
||||||
return &volumeBinder{
|
b := &volumeBinder{
|
||||||
kubeClient: kubeClient,
|
kubeClient: kubeClient,
|
||||||
podLister: podInformer.Lister(),
|
podLister: podInformer.Lister(),
|
||||||
classLister: storageClassInformer.Lister(),
|
classLister: storageClassInformer.Lister(),
|
||||||
@ -197,6 +220,14 @@ func NewVolumeBinder(
|
|||||||
bindTimeout: bindTimeout,
|
bindTimeout: bindTimeout,
|
||||||
translator: csitrans.New(),
|
translator: csitrans.New(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if capacityCheck != nil {
|
||||||
|
b.capacityCheckEnabled = true
|
||||||
|
b.csiDriverLister = capacityCheck.CSIDriverInformer.Lister()
|
||||||
|
b.csiStorageCapacityLister = capacityCheck.CSIStorageCapacityInformer.Lister()
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindPodVolumes finds the matching PVs for PVCs and nodes to provision PVs
|
// FindPodVolumes finds the matching PVs for PVCs and nodes to provision PVs
|
||||||
@ -214,6 +245,7 @@ func (b *volumeBinder) FindPodVolumes(pod *v1.Pod, boundClaims, claimsToBind []*
|
|||||||
// returns without an error.
|
// returns without an error.
|
||||||
unboundVolumesSatisfied := true
|
unboundVolumesSatisfied := true
|
||||||
boundVolumesSatisfied := true
|
boundVolumesSatisfied := true
|
||||||
|
sufficientStorage := true
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -224,6 +256,9 @@ func (b *volumeBinder) FindPodVolumes(pod *v1.Pod, boundClaims, claimsToBind []*
|
|||||||
if !unboundVolumesSatisfied {
|
if !unboundVolumesSatisfied {
|
||||||
reasons = append(reasons, ErrReasonBindConflict)
|
reasons = append(reasons, ErrReasonBindConflict)
|
||||||
}
|
}
|
||||||
|
if !sufficientStorage {
|
||||||
|
reasons = append(reasons, ErrReasonNotEnoughSpace)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
@ -290,9 +325,10 @@ func (b *volumeBinder) FindPodVolumes(pod *v1.Pod, boundClaims, claimsToBind []*
|
|||||||
claimsToProvision = append(claimsToProvision, unboundClaims...)
|
claimsToProvision = append(claimsToProvision, unboundClaims...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for claims to provision
|
// Check for claims to provision. This is the first time where we potentially
|
||||||
|
// find out that storage is not sufficient for the node.
|
||||||
if len(claimsToProvision) > 0 {
|
if len(claimsToProvision) > 0 {
|
||||||
unboundVolumesSatisfied, dynamicProvisions, err = b.checkVolumeProvisions(pod, claimsToProvision, node)
|
unboundVolumesSatisfied, sufficientStorage, dynamicProvisions, err = b.checkVolumeProvisions(pod, claimsToProvision, node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -787,42 +823,51 @@ func (b *volumeBinder) findMatchingVolumes(pod *v1.Pod, claimsToBind []*v1.Persi
|
|||||||
// checkVolumeProvisions checks given unbound claims (the claims have gone through func
|
// checkVolumeProvisions checks given unbound claims (the claims have gone through func
|
||||||
// findMatchingVolumes, and do not have matching volumes for binding), and return true
|
// findMatchingVolumes, and do not have matching volumes for binding), and return true
|
||||||
// if all of the claims are eligible for dynamic provision.
|
// if all of the claims are eligible for dynamic provision.
|
||||||
func (b *volumeBinder) checkVolumeProvisions(pod *v1.Pod, claimsToProvision []*v1.PersistentVolumeClaim, node *v1.Node) (provisionSatisfied bool, dynamicProvisions []*v1.PersistentVolumeClaim, err error) {
|
func (b *volumeBinder) checkVolumeProvisions(pod *v1.Pod, claimsToProvision []*v1.PersistentVolumeClaim, node *v1.Node) (provisionSatisfied, sufficientStorage bool, dynamicProvisions []*v1.PersistentVolumeClaim, err error) {
|
||||||
podName := getPodName(pod)
|
podName := getPodName(pod)
|
||||||
dynamicProvisions = []*v1.PersistentVolumeClaim{}
|
dynamicProvisions = []*v1.PersistentVolumeClaim{}
|
||||||
|
|
||||||
|
// We return early with provisionedClaims == nil if a check
|
||||||
|
// fails or we encounter an error.
|
||||||
for _, claim := range claimsToProvision {
|
for _, claim := range claimsToProvision {
|
||||||
pvcName := getPVCName(claim)
|
pvcName := getPVCName(claim)
|
||||||
className := v1helper.GetPersistentVolumeClaimClass(claim)
|
className := v1helper.GetPersistentVolumeClaimClass(claim)
|
||||||
if className == "" {
|
if className == "" {
|
||||||
return false, nil, fmt.Errorf("no class for claim %q", pvcName)
|
return false, false, nil, fmt.Errorf("no class for claim %q", pvcName)
|
||||||
}
|
}
|
||||||
|
|
||||||
class, err := b.classLister.Get(className)
|
class, err := b.classLister.Get(className)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil, fmt.Errorf("failed to find storage class %q", className)
|
return false, false, nil, fmt.Errorf("failed to find storage class %q", className)
|
||||||
}
|
}
|
||||||
provisioner := class.Provisioner
|
provisioner := class.Provisioner
|
||||||
if provisioner == "" || provisioner == pvutil.NotSupportedProvisioner {
|
if provisioner == "" || provisioner == pvutil.NotSupportedProvisioner {
|
||||||
klog.V(4).Infof("storage class %q of claim %q does not support dynamic provisioning", className, pvcName)
|
klog.V(4).Infof("storage class %q of claim %q does not support dynamic provisioning", className, pvcName)
|
||||||
return false, nil, nil
|
return false, true, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the node can satisfy the topology requirement in the class
|
// Check if the node can satisfy the topology requirement in the class
|
||||||
if !v1helper.MatchTopologySelectorTerms(class.AllowedTopologies, labels.Set(node.Labels)) {
|
if !v1helper.MatchTopologySelectorTerms(class.AllowedTopologies, labels.Set(node.Labels)) {
|
||||||
klog.V(4).Infof("Node %q cannot satisfy provisioning topology requirements of claim %q", node.Name, pvcName)
|
klog.V(4).Infof("Node %q cannot satisfy provisioning topology requirements of claim %q", node.Name, pvcName)
|
||||||
return false, nil, nil
|
return false, true, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Check if capacity of the node domain in the storage class
|
// Check storage capacity.
|
||||||
// can satisfy resource requirement of given claim
|
sufficient, err := b.hasEnoughCapacity(provisioner, claim, class, node)
|
||||||
|
if err != nil {
|
||||||
|
return false, false, nil, err
|
||||||
|
}
|
||||||
|
if !sufficient {
|
||||||
|
// hasEnoughCapacity logs an explanation.
|
||||||
|
return true, false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
dynamicProvisions = append(dynamicProvisions, claim)
|
dynamicProvisions = append(dynamicProvisions, claim)
|
||||||
|
|
||||||
}
|
}
|
||||||
klog.V(4).Infof("Provisioning for %d claims of pod %q that has no matching volumes on node %q ...", len(claimsToProvision), podName, node.Name)
|
klog.V(4).Infof("Provisioning for %d claims of pod %q that has no matching volumes on node %q ...", len(claimsToProvision), podName, node.Name)
|
||||||
|
|
||||||
return true, dynamicProvisions, nil
|
return true, true, dynamicProvisions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *volumeBinder) revertAssumedPVs(bindings []*BindingInfo) {
|
func (b *volumeBinder) revertAssumedPVs(bindings []*BindingInfo) {
|
||||||
@ -837,6 +882,76 @@ func (b *volumeBinder) revertAssumedPVCs(claims []*v1.PersistentVolumeClaim) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasEnoughCapacity checks whether the provisioner has enough capacity left for a new volume of the given size
|
||||||
|
// that is available from the node.
|
||||||
|
func (b *volumeBinder) hasEnoughCapacity(provisioner string, claim *v1.PersistentVolumeClaim, storageClass *storagev1.StorageClass, node *v1.Node) (bool, error) {
|
||||||
|
// This is an optional feature. If disabled, we assume that
|
||||||
|
// there is enough storage.
|
||||||
|
if !b.capacityCheckEnabled {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
quantity, ok := claim.Spec.Resources.Requests[v1.ResourceStorage]
|
||||||
|
if !ok {
|
||||||
|
// No capacity to check for.
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only enabled for CSI drivers which opt into it.
|
||||||
|
driver, err := b.csiDriverLister.Get(provisioner)
|
||||||
|
if err != nil {
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
// Either the provisioner is not a CSI driver or the driver does not
|
||||||
|
// opt into storage capacity scheduling. Either way, skip
|
||||||
|
// capacity checking.
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if driver.Spec.StorageCapacity == nil || !*driver.Spec.StorageCapacity {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for a matching CSIStorageCapacity object(s).
|
||||||
|
// TODO (for beta): benchmark this and potentially introduce some kind of lookup structure (https://github.com/kubernetes/enhancements/issues/1698#issuecomment-654356718).
|
||||||
|
capacities, err := b.csiStorageCapacityLister.List(labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeInBytes := quantity.Value()
|
||||||
|
for _, capacity := range capacities {
|
||||||
|
if capacity.StorageClassName == storageClass.Name &&
|
||||||
|
capacity.Capacity != nil &&
|
||||||
|
capacity.Capacity.Value() >= sizeInBytes &&
|
||||||
|
b.nodeHasAccess(node, capacity) {
|
||||||
|
// Enough capacity found.
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO (?): this doesn't give any information about which pools where considered and why
|
||||||
|
// they had to be rejected. Log that above? But that might be a lot of log output...
|
||||||
|
klog.V(4).Infof("Node %q has no accessible CSIStorageCapacity with enough capacity for PVC %s/%s of size %d and storage class %q",
|
||||||
|
node.Name, claim.Namespace, claim.Name, sizeInBytes, storageClass.Name)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *volumeBinder) nodeHasAccess(node *v1.Node, capacity *storagev1alpha1.CSIStorageCapacity) bool {
|
||||||
|
if capacity.NodeTopology == nil {
|
||||||
|
// Unavailable
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Only matching by label is supported.
|
||||||
|
selector, err := metav1.LabelSelectorAsSelector(capacity.NodeTopology)
|
||||||
|
if err != nil {
|
||||||
|
// This should never happen because NodeTopology must be valid.
|
||||||
|
klog.Errorf("unexpected error converting %+v to a label selector: %v", capacity.NodeTopology, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return selector.Matches(labels.Set(node.Labels))
|
||||||
|
}
|
||||||
|
|
||||||
type byPVCSize []*v1.PersistentVolumeClaim
|
type byPVCSize []*v1.PersistentVolumeClaim
|
||||||
|
|
||||||
func (a byPVCSize) Len() int {
|
func (a byPVCSize) Len() int {
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
storagev1 "k8s.io/api/storage/v1"
|
storagev1 "k8s.io/api/storage/v1"
|
||||||
|
storagev1alpha1 "k8s.io/api/storage/v1alpha1"
|
||||||
"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"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
@ -36,6 +37,7 @@ import (
|
|||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
coreinformers "k8s.io/client-go/informers/core/v1"
|
coreinformers "k8s.io/client-go/informers/core/v1"
|
||||||
storageinformers "k8s.io/client-go/informers/storage/v1"
|
storageinformers "k8s.io/client-go/informers/storage/v1"
|
||||||
|
storageinformersv1alpha1 "k8s.io/client-go/informers/storage/v1alpha1"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
k8stesting "k8s.io/client-go/testing"
|
k8stesting "k8s.io/client-go/testing"
|
||||||
@ -48,6 +50,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
provisioner = "test-provisioner"
|
||||||
|
|
||||||
// PVCs for manual binding
|
// PVCs for manual binding
|
||||||
// TODO: clean up all of these
|
// TODO: clean up all of these
|
||||||
unboundPVC = makeTestPVC("unbound-pvc", "1G", "", pvcUnbound, "", "1", &waitClass)
|
unboundPVC = makeTestPVC("unbound-pvc", "1G", "", pvcUnbound, "", "1", &waitClass)
|
||||||
@ -128,9 +132,13 @@ type testEnv struct {
|
|||||||
internalCSINodeInformer storageinformers.CSINodeInformer
|
internalCSINodeInformer storageinformers.CSINodeInformer
|
||||||
internalPVCache *assumeCache
|
internalPVCache *assumeCache
|
||||||
internalPVCCache *assumeCache
|
internalPVCCache *assumeCache
|
||||||
|
|
||||||
|
// For CSIStorageCapacity feature testing:
|
||||||
|
internalCSIDriverInformer storageinformers.CSIDriverInformer
|
||||||
|
internalCSIStorageCapacityInformer storageinformersv1alpha1.CSIStorageCapacityInformer
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestBinder(t *testing.T, stopCh <-chan struct{}) *testEnv {
|
func newTestBinder(t *testing.T, stopCh <-chan struct{}, csiStorageCapacity ...bool) *testEnv {
|
||||||
client := &fake.Clientset{}
|
client := &fake.Clientset{}
|
||||||
reactor := pvtesting.NewVolumeReactor(client, nil, nil, nil)
|
reactor := pvtesting.NewVolumeReactor(client, nil, nil, nil)
|
||||||
// TODO refactor all tests to use real watch mechanism, see #72327
|
// TODO refactor all tests to use real watch mechanism, see #72327
|
||||||
@ -150,6 +158,15 @@ func newTestBinder(t *testing.T, stopCh <-chan struct{}) *testEnv {
|
|||||||
csiNodeInformer := informerFactory.Storage().V1().CSINodes()
|
csiNodeInformer := informerFactory.Storage().V1().CSINodes()
|
||||||
pvcInformer := informerFactory.Core().V1().PersistentVolumeClaims()
|
pvcInformer := informerFactory.Core().V1().PersistentVolumeClaims()
|
||||||
classInformer := informerFactory.Storage().V1().StorageClasses()
|
classInformer := informerFactory.Storage().V1().StorageClasses()
|
||||||
|
csiDriverInformer := informerFactory.Storage().V1().CSIDrivers()
|
||||||
|
csiStorageCapacityInformer := informerFactory.Storage().V1alpha1().CSIStorageCapacities()
|
||||||
|
var capacityCheck *CapacityCheck
|
||||||
|
if len(csiStorageCapacity) > 0 && csiStorageCapacity[0] {
|
||||||
|
capacityCheck = &CapacityCheck{
|
||||||
|
CSIDriverInformer: csiDriverInformer,
|
||||||
|
CSIStorageCapacityInformer: csiStorageCapacityInformer,
|
||||||
|
}
|
||||||
|
}
|
||||||
binder := NewVolumeBinder(
|
binder := NewVolumeBinder(
|
||||||
client,
|
client,
|
||||||
podInformer,
|
podInformer,
|
||||||
@ -158,6 +175,7 @@ func newTestBinder(t *testing.T, stopCh <-chan struct{}) *testEnv {
|
|||||||
pvcInformer,
|
pvcInformer,
|
||||||
informerFactory.Core().V1().PersistentVolumes(),
|
informerFactory.Core().V1().PersistentVolumes(),
|
||||||
classInformer,
|
classInformer,
|
||||||
|
capacityCheck,
|
||||||
10*time.Second)
|
10*time.Second)
|
||||||
|
|
||||||
// Wait for informers cache sync
|
// Wait for informers cache sync
|
||||||
@ -177,7 +195,7 @@ func newTestBinder(t *testing.T, stopCh <-chan struct{}) *testEnv {
|
|||||||
Name: waitClassWithProvisioner,
|
Name: waitClassWithProvisioner,
|
||||||
},
|
},
|
||||||
VolumeBindingMode: &waitMode,
|
VolumeBindingMode: &waitMode,
|
||||||
Provisioner: "test-provisioner",
|
Provisioner: provisioner,
|
||||||
AllowedTopologies: []v1.TopologySelectorTerm{
|
AllowedTopologies: []v1.TopologySelectorTerm{
|
||||||
{
|
{
|
||||||
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
|
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
|
||||||
@ -207,7 +225,7 @@ func newTestBinder(t *testing.T, stopCh <-chan struct{}) *testEnv {
|
|||||||
Name: topoMismatchClass,
|
Name: topoMismatchClass,
|
||||||
},
|
},
|
||||||
VolumeBindingMode: &waitMode,
|
VolumeBindingMode: &waitMode,
|
||||||
Provisioner: "test-provisioner",
|
Provisioner: provisioner,
|
||||||
AllowedTopologies: []v1.TopologySelectorTerm{
|
AllowedTopologies: []v1.TopologySelectorTerm{
|
||||||
{
|
{
|
||||||
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
|
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
|
||||||
@ -254,6 +272,9 @@ func newTestBinder(t *testing.T, stopCh <-chan struct{}) *testEnv {
|
|||||||
internalCSINodeInformer: csiNodeInformer,
|
internalCSINodeInformer: csiNodeInformer,
|
||||||
internalPVCache: internalPVCache,
|
internalPVCache: internalPVCache,
|
||||||
internalPVCCache: internalPVCCache,
|
internalPVCCache: internalPVCCache,
|
||||||
|
|
||||||
|
internalCSIDriverInformer: csiDriverInformer,
|
||||||
|
internalCSIStorageCapacityInformer: csiStorageCapacityInformer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,6 +292,18 @@ func (env *testEnv) initCSINodes(cachedCSINodes []*storagev1.CSINode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (env *testEnv) addCSIDriver(csiDriver *storagev1.CSIDriver) {
|
||||||
|
csiDriverInformer := env.internalCSIDriverInformer.Informer()
|
||||||
|
csiDriverInformer.GetIndexer().Add(csiDriver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (env *testEnv) addCSIStorageCapacities(capacities []*storagev1alpha1.CSIStorageCapacity) {
|
||||||
|
csiStorageCapacityInformer := env.internalCSIStorageCapacityInformer.Informer()
|
||||||
|
for _, capacity := range capacities {
|
||||||
|
csiStorageCapacityInformer.GetIndexer().Add(capacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (env *testEnv) initClaims(cachedPVCs []*v1.PersistentVolumeClaim, apiPVCs []*v1.PersistentVolumeClaim) {
|
func (env *testEnv) initClaims(cachedPVCs []*v1.PersistentVolumeClaim, apiPVCs []*v1.PersistentVolumeClaim) {
|
||||||
internalPVCCache := env.internalPVCCache
|
internalPVCCache := env.internalPVCCache
|
||||||
for _, pvc := range cachedPVCs {
|
for _, pvc := range cachedPVCs {
|
||||||
@ -678,6 +711,35 @@ func makeCSINode(name, migratedPlugin string) *storagev1.CSINode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeCSIDriver(name string, storageCapacity bool) *storagev1.CSIDriver {
|
||||||
|
return &storagev1.CSIDriver{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
Spec: storagev1.CSIDriverSpec{
|
||||||
|
StorageCapacity: &storageCapacity,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCapacity(name, storageClassName string, node *v1.Node, capacityStr string) *storagev1alpha1.CSIStorageCapacity {
|
||||||
|
c := &storagev1alpha1.CSIStorageCapacity{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
StorageClassName: storageClassName,
|
||||||
|
NodeTopology: &metav1.LabelSelector{},
|
||||||
|
}
|
||||||
|
if node != nil {
|
||||||
|
c.NodeTopology.MatchLabels = map[string]string{nodeLabelKey: node.Labels[nodeLabelKey]}
|
||||||
|
}
|
||||||
|
if capacityStr != "" {
|
||||||
|
capacityQuantity := resource.MustParse(capacityStr)
|
||||||
|
c.Capacity = &capacityQuantity
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
func makePod(pvcs []*v1.PersistentVolumeClaim) *v1.Pod {
|
func makePod(pvcs []*v1.PersistentVolumeClaim) *v1.Pod {
|
||||||
pod := &v1.Pod{
|
pod := &v1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
@ -745,6 +807,8 @@ func reasonNames(reasons ConflictReasons) string {
|
|||||||
varNames = append(varNames, "ErrReasonBindConflict")
|
varNames = append(varNames, "ErrReasonBindConflict")
|
||||||
case ErrReasonNodeConflict:
|
case ErrReasonNodeConflict:
|
||||||
varNames = append(varNames, "ErrReasonNodeConflict")
|
varNames = append(varNames, "ErrReasonNodeConflict")
|
||||||
|
case ErrReasonNotEnoughSpace:
|
||||||
|
varNames = append(varNames, "ErrReasonNotEnoughSpace")
|
||||||
default:
|
default:
|
||||||
varNames = append(varNames, string(reason))
|
varNames = append(varNames, string(reason))
|
||||||
}
|
}
|
||||||
@ -897,13 +961,16 @@ func TestFindPodVolumesWithoutProvisioning(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
run := func(t *testing.T, scenario scenarioType) {
|
run := func(t *testing.T, scenario scenarioType, csiStorageCapacity bool, csiDriver *storagev1.CSIDriver) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
testEnv := newTestBinder(t, ctx.Done())
|
testEnv := newTestBinder(t, ctx.Done(), csiStorageCapacity)
|
||||||
testEnv.initVolumes(scenario.pvs, scenario.pvs)
|
testEnv.initVolumes(scenario.pvs, scenario.pvs)
|
||||||
|
if csiDriver != nil {
|
||||||
|
testEnv.addCSIDriver(csiDriver)
|
||||||
|
}
|
||||||
|
|
||||||
// a. Init pvc cache
|
// a. Init pvc cache
|
||||||
if scenario.cachePVCs == nil {
|
if scenario.cachePVCs == nil {
|
||||||
@ -930,8 +997,20 @@ func TestFindPodVolumesWithoutProvisioning(t *testing.T) {
|
|||||||
testEnv.validatePodCache(t, testNode.Name, scenario.pod, podVolumes, scenario.expectedBindings, nil)
|
testEnv.validatePodCache(t, testNode.Name, scenario.pod, podVolumes, scenario.expectedBindings, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, scenario := range scenarios {
|
for prefix, csiStorageCapacity := range map[string]bool{"with": true, "without": false} {
|
||||||
t.Run(name, func(t *testing.T) { run(t, scenario) })
|
t.Run(fmt.Sprintf("%s CSIStorageCapacity", prefix), func(t *testing.T) {
|
||||||
|
for description, csiDriver := range map[string]*storagev1.CSIDriver{
|
||||||
|
"no CSIDriver": nil,
|
||||||
|
"CSIDriver with capacity tracking": makeCSIDriver(provisioner, true),
|
||||||
|
"CSIDriver without capacity tracking": makeCSIDriver(provisioner, false),
|
||||||
|
} {
|
||||||
|
t.Run(description, func(t *testing.T) {
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) { run(t, scenario, csiStorageCapacity, csiDriver) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -950,29 +1029,34 @@ func TestFindPodVolumesWithProvisioning(t *testing.T) {
|
|||||||
expectedProvisions []*v1.PersistentVolumeClaim
|
expectedProvisions []*v1.PersistentVolumeClaim
|
||||||
|
|
||||||
// Expected return values
|
// Expected return values
|
||||||
reasons ConflictReasons
|
reasons ConflictReasons
|
||||||
shouldFail bool
|
shouldFail bool
|
||||||
|
needsCapacity bool
|
||||||
}
|
}
|
||||||
scenarios := map[string]scenarioType{
|
scenarios := map[string]scenarioType{
|
||||||
"one-provisioned": {
|
"one-provisioned": {
|
||||||
podPVCs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
podPVCs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC},
|
expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
needsCapacity: true,
|
||||||
},
|
},
|
||||||
"two-unbound-pvcs,one-matched,one-provisioned": {
|
"two-unbound-pvcs,one-matched,one-provisioned": {
|
||||||
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, provisionedPVC},
|
podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, provisionedPVC},
|
||||||
pvs: []*v1.PersistentVolume{pvNode1a},
|
pvs: []*v1.PersistentVolume{pvNode1a},
|
||||||
expectedBindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a)},
|
expectedBindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a)},
|
||||||
expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC},
|
expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
needsCapacity: true,
|
||||||
},
|
},
|
||||||
"one-bound,one-provisioned": {
|
"one-bound,one-provisioned": {
|
||||||
podPVCs: []*v1.PersistentVolumeClaim{boundPVC, provisionedPVC},
|
podPVCs: []*v1.PersistentVolumeClaim{boundPVC, provisionedPVC},
|
||||||
pvs: []*v1.PersistentVolume{pvBound},
|
pvs: []*v1.PersistentVolume{pvBound},
|
||||||
expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC},
|
expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
needsCapacity: true,
|
||||||
},
|
},
|
||||||
"one-binding,one-selected-node": {
|
"one-binding,one-selected-node": {
|
||||||
podPVCs: []*v1.PersistentVolumeClaim{boundPVC, selectedNodePVC},
|
podPVCs: []*v1.PersistentVolumeClaim{boundPVC, selectedNodePVC},
|
||||||
pvs: []*v1.PersistentVolume{pvBound},
|
pvs: []*v1.PersistentVolume{pvBound},
|
||||||
expectedProvisions: []*v1.PersistentVolumeClaim{selectedNodePVC},
|
expectedProvisions: []*v1.PersistentVolumeClaim{selectedNodePVC},
|
||||||
|
needsCapacity: true,
|
||||||
},
|
},
|
||||||
"immediate-unbound-pvc": {
|
"immediate-unbound-pvc": {
|
||||||
podPVCs: []*v1.PersistentVolumeClaim{immediateUnboundPVC},
|
podPVCs: []*v1.PersistentVolumeClaim{immediateUnboundPVC},
|
||||||
@ -982,6 +1066,7 @@ func TestFindPodVolumesWithProvisioning(t *testing.T) {
|
|||||||
podPVCs: []*v1.PersistentVolumeClaim{immediateBoundPVC, provisionedPVC},
|
podPVCs: []*v1.PersistentVolumeClaim{immediateBoundPVC, provisionedPVC},
|
||||||
pvs: []*v1.PersistentVolume{pvBoundImmediate},
|
pvs: []*v1.PersistentVolume{pvBoundImmediate},
|
||||||
expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC},
|
expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
needsCapacity: true,
|
||||||
},
|
},
|
||||||
"invalid-provisioner": {
|
"invalid-provisioner": {
|
||||||
podPVCs: []*v1.PersistentVolumeClaim{noProvisionerPVC},
|
podPVCs: []*v1.PersistentVolumeClaim{noProvisionerPVC},
|
||||||
@ -1002,13 +1087,16 @@ func TestFindPodVolumesWithProvisioning(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
run := func(t *testing.T, scenario scenarioType) {
|
run := func(t *testing.T, scenario scenarioType, csiStorageCapacity bool, csiDriver *storagev1.CSIDriver) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
testEnv := newTestBinder(t, ctx.Done())
|
testEnv := newTestBinder(t, ctx.Done(), csiStorageCapacity)
|
||||||
testEnv.initVolumes(scenario.pvs, scenario.pvs)
|
testEnv.initVolumes(scenario.pvs, scenario.pvs)
|
||||||
|
if csiDriver != nil {
|
||||||
|
testEnv.addCSIDriver(csiDriver)
|
||||||
|
}
|
||||||
|
|
||||||
// a. Init pvc cache
|
// a. Init pvc cache
|
||||||
if scenario.cachePVCs == nil {
|
if scenario.cachePVCs == nil {
|
||||||
@ -1031,12 +1119,32 @@ func TestFindPodVolumesWithProvisioning(t *testing.T) {
|
|||||||
if scenario.shouldFail && err == nil {
|
if scenario.shouldFail && err == nil {
|
||||||
t.Error("returned success but expected error")
|
t.Error("returned success but expected error")
|
||||||
}
|
}
|
||||||
checkReasons(t, reasons, scenario.reasons)
|
expectedReasons := scenario.reasons
|
||||||
testEnv.validatePodCache(t, testNode.Name, scenario.pod, podVolumes, scenario.expectedBindings, scenario.expectedProvisions)
|
expectedProvisions := scenario.expectedProvisions
|
||||||
|
if scenario.needsCapacity && csiStorageCapacity &&
|
||||||
|
csiDriver != nil && csiDriver.Spec.StorageCapacity != nil && *csiDriver.Spec.StorageCapacity {
|
||||||
|
// Without CSIStorageCapacity objects, provisioning is blocked.
|
||||||
|
expectedReasons = append(expectedReasons, ErrReasonNotEnoughSpace)
|
||||||
|
expectedProvisions = nil
|
||||||
|
}
|
||||||
|
checkReasons(t, reasons, expectedReasons)
|
||||||
|
testEnv.validatePodCache(t, testNode.Name, scenario.pod, podVolumes, scenario.expectedBindings, expectedProvisions)
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, scenario := range scenarios {
|
for prefix, csiStorageCapacity := range map[string]bool{"with": true, "without": false} {
|
||||||
t.Run(name, func(t *testing.T) { run(t, scenario) })
|
t.Run(fmt.Sprintf("%s CSIStorageCapacity", prefix), func(t *testing.T) {
|
||||||
|
for description, csiDriver := range map[string]*storagev1.CSIDriver{
|
||||||
|
"no CSIDriver": nil,
|
||||||
|
"CSIDriver with capacity tracking": makeCSIDriver(provisioner, true),
|
||||||
|
"CSIDriver without capacity tracking": makeCSIDriver(provisioner, false),
|
||||||
|
} {
|
||||||
|
t.Run(description, func(t *testing.T) {
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) { run(t, scenario, csiStorageCapacity, csiDriver) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2008,3 +2116,125 @@ func TestFindAssumeVolumes(t *testing.T) {
|
|||||||
testEnv.validatePodCache(t, testNode.Name, pod, podVolumes, expectedBindings, nil)
|
testEnv.validatePodCache(t, testNode.Name, pod, podVolumes, expectedBindings, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCapacity covers different scenarios involving CSIStorageCapacity objects.
|
||||||
|
// Scenarios without those are covered by TestFindPodVolumesWithProvisioning.
|
||||||
|
func TestCapacity(t *testing.T) {
|
||||||
|
type scenarioType struct {
|
||||||
|
// Inputs
|
||||||
|
pvcs []*v1.PersistentVolumeClaim
|
||||||
|
capacities []*storagev1alpha1.CSIStorageCapacity
|
||||||
|
|
||||||
|
// Expected return values
|
||||||
|
reasons ConflictReasons
|
||||||
|
shouldFail bool
|
||||||
|
}
|
||||||
|
scenarios := map[string]scenarioType{
|
||||||
|
"network-attached": {
|
||||||
|
pvcs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
capacities: []*storagev1alpha1.CSIStorageCapacity{
|
||||||
|
makeCapacity("net", waitClassWithProvisioner, nil, "1Gi"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"local-storage": {
|
||||||
|
pvcs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
capacities: []*storagev1alpha1.CSIStorageCapacity{
|
||||||
|
makeCapacity("net", waitClassWithProvisioner, node1, "1Gi"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple": {
|
||||||
|
pvcs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
capacities: []*storagev1alpha1.CSIStorageCapacity{
|
||||||
|
makeCapacity("net", waitClassWithProvisioner, nil, "1Gi"),
|
||||||
|
makeCapacity("net", waitClassWithProvisioner, node2, "1Gi"),
|
||||||
|
makeCapacity("net", waitClassWithProvisioner, node1, "1Gi"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"no-storage": {
|
||||||
|
pvcs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
reasons: ConflictReasons{ErrReasonNotEnoughSpace},
|
||||||
|
},
|
||||||
|
"wrong-node": {
|
||||||
|
pvcs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
capacities: []*storagev1alpha1.CSIStorageCapacity{
|
||||||
|
makeCapacity("net", waitClassWithProvisioner, node2, "1Gi"),
|
||||||
|
},
|
||||||
|
reasons: ConflictReasons{ErrReasonNotEnoughSpace},
|
||||||
|
},
|
||||||
|
"wrong-storage-class": {
|
||||||
|
pvcs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
capacities: []*storagev1alpha1.CSIStorageCapacity{
|
||||||
|
makeCapacity("net", waitClass, node1, "1Gi"),
|
||||||
|
},
|
||||||
|
reasons: ConflictReasons{ErrReasonNotEnoughSpace},
|
||||||
|
},
|
||||||
|
"insufficient-storage": {
|
||||||
|
pvcs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
capacities: []*storagev1alpha1.CSIStorageCapacity{
|
||||||
|
makeCapacity("net", waitClassWithProvisioner, node1, "1Mi"),
|
||||||
|
},
|
||||||
|
reasons: ConflictReasons{ErrReasonNotEnoughSpace},
|
||||||
|
},
|
||||||
|
"zero-storage": {
|
||||||
|
pvcs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
capacities: []*storagev1alpha1.CSIStorageCapacity{
|
||||||
|
makeCapacity("net", waitClassWithProvisioner, node1, "0Mi"),
|
||||||
|
},
|
||||||
|
reasons: ConflictReasons{ErrReasonNotEnoughSpace},
|
||||||
|
},
|
||||||
|
"nil-storage": {
|
||||||
|
pvcs: []*v1.PersistentVolumeClaim{provisionedPVC},
|
||||||
|
capacities: []*storagev1alpha1.CSIStorageCapacity{
|
||||||
|
makeCapacity("net", waitClassWithProvisioner, node1, ""),
|
||||||
|
},
|
||||||
|
reasons: ConflictReasons{ErrReasonNotEnoughSpace},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testNode := &v1.Node{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "node1",
|
||||||
|
Labels: map[string]string{
|
||||||
|
nodeLabelKey: "node1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
run := func(t *testing.T, scenario scenarioType) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
withCSIStorageCapacity := true
|
||||||
|
testEnv := newTestBinder(t, ctx.Done(), withCSIStorageCapacity)
|
||||||
|
testEnv.addCSIDriver(makeCSIDriver(provisioner, withCSIStorageCapacity))
|
||||||
|
testEnv.addCSIStorageCapacities(scenario.capacities)
|
||||||
|
|
||||||
|
// a. Init pvc cache
|
||||||
|
testEnv.initClaims(scenario.pvcs, scenario.pvcs)
|
||||||
|
|
||||||
|
// b. Generate pod with given claims
|
||||||
|
pod := makePod(scenario.pvcs)
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
podVolumes, reasons, err := findPodVolumes(testEnv.binder, pod, testNode)
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
if !scenario.shouldFail && err != nil {
|
||||||
|
t.Errorf("returned error: %v", err)
|
||||||
|
}
|
||||||
|
if scenario.shouldFail && err == nil {
|
||||||
|
t.Error("returned success but expected error")
|
||||||
|
}
|
||||||
|
checkReasons(t, reasons, scenario.reasons)
|
||||||
|
provisions := scenario.pvcs
|
||||||
|
if len(reasons) > 0 {
|
||||||
|
provisions = nil
|
||||||
|
}
|
||||||
|
testEnv.validatePodCache(t, pod.Spec.NodeName, pod, podVolumes, nil, provisions)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) { run(t, scenario) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,10 +7,12 @@ go_library(
|
|||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
"//pkg/controller/volume/scheduling:go_default_library",
|
"//pkg/controller/volume/scheduling:go_default_library",
|
||||||
|
"//pkg/features:go_default_library",
|
||||||
"//pkg/scheduler/apis/config:go_default_library",
|
"//pkg/scheduler/apis/config:go_default_library",
|
||||||
"//pkg/scheduler/framework/v1alpha1:go_default_library",
|
"//pkg/scheduler/framework/v1alpha1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||||
"//vendor/k8s.io/klog/v2:go_default_library",
|
"//vendor/k8s.io/klog/v2:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -24,8 +24,10 @@ import (
|
|||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
"k8s.io/kubernetes/pkg/controller/volume/scheduling"
|
"k8s.io/kubernetes/pkg/controller/volume/scheduling"
|
||||||
|
"k8s.io/kubernetes/pkg/features"
|
||||||
"k8s.io/kubernetes/pkg/scheduler/apis/config"
|
"k8s.io/kubernetes/pkg/scheduler/apis/config"
|
||||||
framework "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1"
|
framework "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1"
|
||||||
)
|
)
|
||||||
@ -136,6 +138,9 @@ func getStateData(cs *framework.CycleState) (*stateData, error) {
|
|||||||
// For PVCs that are unbound, it tries to find available PVs that can satisfy the PVC requirements
|
// For PVCs that are unbound, it tries to find available PVs that can satisfy the PVC requirements
|
||||||
// and that the PV node affinity is satisfied by the given node.
|
// and that the PV node affinity is satisfied by the given node.
|
||||||
//
|
//
|
||||||
|
// If storage capacity tracking is enabled, then enough space has to be available
|
||||||
|
// for the node and volumes that still need to be created.
|
||||||
|
//
|
||||||
// The predicate returns true if all bound PVCs have compatible PVs with the node, and if all unbound
|
// The predicate returns true if all bound PVCs have compatible PVs with the node, and if all unbound
|
||||||
// PVCs can be matched with an available and node-compatible PV.
|
// PVCs can be matched with an available and node-compatible PV.
|
||||||
func (pl *VolumeBinding) Filter(ctx context.Context, cs *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
|
func (pl *VolumeBinding) Filter(ctx context.Context, cs *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
|
||||||
@ -254,7 +259,14 @@ func New(plArgs runtime.Object, fh framework.FrameworkHandle) (framework.Plugin,
|
|||||||
pvInformer := fh.SharedInformerFactory().Core().V1().PersistentVolumes()
|
pvInformer := fh.SharedInformerFactory().Core().V1().PersistentVolumes()
|
||||||
storageClassInformer := fh.SharedInformerFactory().Storage().V1().StorageClasses()
|
storageClassInformer := fh.SharedInformerFactory().Storage().V1().StorageClasses()
|
||||||
csiNodeInformer := fh.SharedInformerFactory().Storage().V1().CSINodes()
|
csiNodeInformer := fh.SharedInformerFactory().Storage().V1().CSINodes()
|
||||||
binder := scheduling.NewVolumeBinder(fh.ClientSet(), podInformer, nodeInformer, csiNodeInformer, pvcInformer, pvInformer, storageClassInformer, time.Duration(args.BindTimeoutSeconds)*time.Second)
|
var capacityCheck *scheduling.CapacityCheck
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(features.CSIStorageCapacity) {
|
||||||
|
capacityCheck = &scheduling.CapacityCheck{
|
||||||
|
CSIDriverInformer: fh.SharedInformerFactory().Storage().V1().CSIDrivers(),
|
||||||
|
CSIStorageCapacityInformer: fh.SharedInformerFactory().Storage().V1alpha1().CSIStorageCapacities(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binder := scheduling.NewVolumeBinder(fh.ClientSet(), podInformer, nodeInformer, csiNodeInformer, pvcInformer, pvInformer, storageClassInformer, capacityCheck, time.Duration(args.BindTimeoutSeconds)*time.Second)
|
||||||
return &VolumeBinding{
|
return &VolumeBinding{
|
||||||
Binder: binder,
|
Binder: binder,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -541,6 +541,12 @@ func ClusterRoles() []rbacv1.ClusterRole {
|
|||||||
// Needed for volume limits
|
// Needed for volume limits
|
||||||
rbacv1helpers.NewRule(Read...).Groups(storageGroup).Resources("csinodes").RuleOrDie(),
|
rbacv1helpers.NewRule(Read...).Groups(storageGroup).Resources("csinodes").RuleOrDie(),
|
||||||
}
|
}
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(features.CSIStorageCapacity) {
|
||||||
|
kubeSchedulerRules = append(kubeSchedulerRules,
|
||||||
|
rbacv1helpers.NewRule(Read...).Groups(storageGroup).Resources("csidrivers").RuleOrDie(),
|
||||||
|
rbacv1helpers.NewRule(Read...).Groups(storageGroup).Resources("csistoragecapacities").RuleOrDie(),
|
||||||
|
)
|
||||||
|
}
|
||||||
roles = append(roles, rbacv1.ClusterRole{
|
roles = append(roles, rbacv1.ClusterRole{
|
||||||
// a role to use for the kube-scheduler
|
// a role to use for the kube-scheduler
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "system:kube-scheduler"},
|
ObjectMeta: metav1.ObjectMeta{Name: "system:kube-scheduler"},
|
||||||
|
Loading…
Reference in New Issue
Block a user