Change:
}, } <ins> pendingVolumePod := &v1.Pod{ Spec: v1.PodSpec{ Volumes: []v1.Volume{ { VolumeSource: v1.VolumeSource{ PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ ClaimName: "csi-4", }, }, }, }, }, } // Different pod than pendingVolumePod, but using the same unbound PVC unboundPVCPod2 := &v1.Pod{ Spec: v1.PodSpec{ Volumes: []v1.Volume{ { VolumeSource: v1.VolumeSource{ PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ ClaimName: "csi-4", }, }, }, }, }, } missingPVPod := &v1.Pod{ Spec: v1.PodSpec{ Volumes: []v1.Volume{ { VolumeSource: v1.VolumeSource{ PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ ClaimName: "csi-6", }, }, }, }, }, } noSCPVCPod := &v1.Pod{ Spec: v1.PodSpec{ Volumes: []v1.Volume{ { VolumeSource: v1.VolumeSource{ PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ ClaimName: "csi-5", }, }, }, }, }, } gceTwoVolPod := &v1.Pod{ Spec: v1.PodSpec{ Volumes: []v1.Volume{ { VolumeSource: v1.VolumeSource{ PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ ClaimName: "cs-gce-1", }, }, }, { VolumeSource: v1.VolumeSource{ PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ ClaimName: "csi-gce-2", }, }, }, }, }, } </ins> tests := []struct { newPod       *v1.Pod existingPods []*v1.Pod filterName   string maxVols      int <ins> driverNames  []string </ins> fits         bool test         string }{ { newPod:       oneVolPod, existingPods: []*v1.Pod{runningPod, twoVolPod}, <del> filterName:   "csi-ebs", </del> <ins> filterName:   "csi", </ins> maxVols:      4, <ins> driverNames:  []string{"ebs"}, </ins> fits:         true, test:         "fits when node capacity >= new pods CSI volume", }, { newPod:       oneVolPod, existingPods: []*v1.Pod{runningPod, twoVolPod}, <del> filterName:   "csi-ebs", </del> <ins> filterName:   "csi", </ins> maxVols:      2, <ins> driverNames:  []string{"ebs"}, </ins> fits:         false, test:         "doesn't when node capacity <= pods CSI volume", }, <ins> // should count pending PVCs { newPod:       oneVolPod, existingPods: []*v1.Pod{pendingVolumePod, twoVolPod}, filterName:   "csi", maxVols:      2, driverNames:  []string{"ebs"}, fits:         false, test:         "count pending PVCs towards capacity <= pods CSI volume", }, // two same pending PVCs should be counted as 1 { newPod:       oneVolPod, existingPods: []*v1.Pod{pendingVolumePod, unboundPVCPod2, twoVolPod}, filterName:   "csi", maxVols:      3, driverNames:  []string{"ebs"}, fits:         true, test:         "count multiple pending pvcs towards capacity >= pods CSI volume", }, // should count PVCs with invalid PV name but valid SC { newPod:       oneVolPod, existingPods: []*v1.Pod{missingPVPod, twoVolPod}, filterName:   "csi", maxVols:      2, driverNames:  []string{"ebs"}, fits:         false, test:         "should count PVCs with invalid PV name but valid SC", }, // don't count a volume which has storageclass missing { newPod:       oneVolPod, existingPods: []*v1.Pod{runningPod, noSCPVCPod}, filterName:   "csi", maxVols:      2, driverNames:  []string{"ebs"}, fits:         true, test:         "don't count pvcs with missing SC towards capacity", }, // don't count multiple volume types { newPod:       oneVolPod, existingPods: []*v1.Pod{gceTwoVolPod, twoVolPod}, filterName:   "csi", maxVols:      2, driverNames:  []string{"ebs", "gce"}, fits:         true, test:         "don't count pvcs with different type towards capacity", }, { newPod:       gceTwoVolPod, existingPods: []*v1.Pod{twoVolPod, runningPod}, filterName:   "csi", maxVols:      2, driverNames:  []string{"ebs", "gce"}, fits:         true, test:         "don't count pvcs with different type towards capacity", }, </ins> } defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AttachVolumeLimit, true)() expectedFailureReasons := []PredicateFailureReason{ErrMaxVolumeCountExceeded} // running attachable predicate tests with feature gate and limit present on nodes for _, test := range tests { <del> node := getNodeWithPodAndVolumeLimits(test.existingPods, int64(test.maxVols), test.filterName) pred := NewCSIMaxVolumeLimitPredicate(getFakeCSIPVInfo("csi-ebs", "csi-ebs"), getFakeCSIPVCInfo("csi-ebs")) </del> <ins> node := getNodeWithPodAndVolumeLimits(test.existingPods, int64(test.maxVols), test.driverNames...) pred := NewCSIMaxVolumeLimitPredicate(getFakeCSIPVInfo(test.filterName, test.driverNames...), getFakeCSIPVCInfo(test.filterName, "csi-sc", test.driverNames...), getFakeCSIStorageClassInfo("csi-sc", test.driverNames[0])) </ins> fits, reasons, err := pred(test.newPod, GetPredicateMetadata(test.newPod, nil), node) if err != nil { t.Errorf("Using allocatable [%s]%s: unexpected error: %v", test.filterName, test.test, err)

Discussion:
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!--What happened: Volume binding mode doesn't consider the volume attach limit per node. What you expected to happen: volume binding mode should have respected the attach limit, similar to binding mode. How to reproduce it (as minimally and precisely as possible): In CSI driver, configured the volume-attach limit to 1 per node on a 3 node cluster. In volume binding mode, only 3 pods with 3 PV/PVCs runs. In binding mode, all 5 pods runs with 5PV/PVCs on 3 node cluster. (Actually 4 pods scheduled on 1 node and 1 on other node, 3rd didn't have any pod scheduled) Anything else we need to know?: I was able to reproduced it with minimal node and configuration, and also hit this for stress case with 100 Volume limit and 500 pods running on the 5 mode cluster. Environment: Kubernetes version (use ): ~ kubectl version Client Version: version.Info{Major:"1", Minor:"12", GitVersion:"v1.12.2", GitCommit:"", GitTreeState:"clean", BuildDate:"2018-10-30T21:39:38Z", GoVersion:"go1.11.1", Compiler:"gc", Platform:"darwin/amd64"} Server Version: version.Info{Major:"1", Minor:"13", GitVersion:"v1.13.0", GitCommit:"", GitTreeState:"clean", BuildDate:"2018-12-03T20:56:12Z", GoVersion:"go1.11.2", Compiler:"gc", Platform:"linux/amd64"} Cloud provider or hardware configuration: On-prem linux server OS (e.g. from /etc/os-release): Centos 7Kernel (e.g. ): 3.10.0 Install tools: Others:
cc
/sig storage
/assign Currently the predicate PVCs that are not bound. It should lookup the storageclass to get the provisioner/driver name. This solution will require a StorageClass object is even if you don't use dynamic provisioning.

I had a thought that it wouldn't be so simple to count unbound PVCs of other pods since binding is still in progress, but I think it's fine actually. We only count other pods that have been assigned to the same node. Those pods are assigned to the node AND still unbound because of one of these reasons: It's actually been scheduled to the node and someone manually came in and unbound the PVC: unlikely and an unsupported sequence since we implemented StorageProtection The Pod was initially created with nodeName already set, bypassing the scheduler The Pod is going to be scheduled (it's been assumed), and it's currently going through binding. This is normal with delayed binding. Either way, the fact that the Pod's nodename is set to this node means that the pod should be counting towards the node's limit.
Hmm, so after opening the PR that fixes this, I realize that - from storageclass we don't really know if the volume we are evaluating is CSI volume or not. I think a similar problem exists for existing predicate though. Like AzureDisk predicate does not know if PVC it is over counting is of type AzureVolume or not. So, I suspect that those 4 hardcoded predicates are counting all delayed binding volumes.
if the storageclass.provisioner name is not found, can we just ignore it?
I think we can ignore it. The PR currently does that.