EvenPodsSpread: PredicateMetadata initilization
- build a new `topologyPairsPodSpreadMap` into PredicateMetadata - update ShallowCopy() - unit tests
This commit is contained in:
		| @@ -34,6 +34,9 @@ import ( | |||||||
| 	schedutil "k8s.io/kubernetes/pkg/scheduler/util" | 	schedutil "k8s.io/kubernetes/pkg/scheduler/util" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // MaxInt32 is the maximum value of int32 | ||||||
|  | const MaxInt32 = int32(^uint32(0) >> 1) | ||||||
|  |  | ||||||
| // PredicateMetadata interface represents anything that can access a predicate metadata. | // PredicateMetadata interface represents anything that can access a predicate metadata. | ||||||
| type PredicateMetadata interface { | type PredicateMetadata interface { | ||||||
| 	ShallowCopy() PredicateMetadata | 	ShallowCopy() PredicateMetadata | ||||||
| @@ -66,6 +69,21 @@ type topologyPairsMaps struct { | |||||||
| 	podToTopologyPairs map[string]topologyPairSet | 	podToTopologyPairs map[string]topologyPairSet | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // topologyPairsPodSpreadMap combines []int32 and topologyPairsMaps to represent | ||||||
|  | // (1) how existing pods match incoming pod on its spread constraints | ||||||
|  | // (2) minimum match number of each hard spread constraint | ||||||
|  | type topologyPairsPodSpreadMap struct { | ||||||
|  | 	minMatches []int32 | ||||||
|  | 	*topologyPairsMaps | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newTopologyPairsPodSpreadMap() *topologyPairsPodSpreadMap { | ||||||
|  | 	return &topologyPairsPodSpreadMap{ | ||||||
|  | 		// minMatches will be initilized with proper size later | ||||||
|  | 		topologyPairsMaps: newTopologyPairsMaps(), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| // NOTE: When new fields are added/removed or logic is changed, please make sure that | // NOTE: When new fields are added/removed or logic is changed, please make sure that | ||||||
| // RemovePod, AddPod, and ShallowCopy functions are updated to work with the new changes. | // RemovePod, AddPod, and ShallowCopy functions are updated to work with the new changes. | ||||||
| type predicateMetadata struct { | type predicateMetadata struct { | ||||||
| @@ -91,6 +109,9 @@ type predicateMetadata struct { | |||||||
| 	// which should be accounted only by the extenders. This set is synthesized | 	// which should be accounted only by the extenders. This set is synthesized | ||||||
| 	// from scheduler extender configuration and does not change per pod. | 	// from scheduler extender configuration and does not change per pod. | ||||||
| 	ignoredExtendedResources sets.String | 	ignoredExtendedResources sets.String | ||||||
|  | 	// Similar like map for pod (anti-)affinity, but impose additional min matches info | ||||||
|  | 	// to describe mininum match number on each topology spread constraint | ||||||
|  | 	topologyPairsPodSpreadMap *topologyPairsPodSpreadMap | ||||||
| } | } | ||||||
|  |  | ||||||
| // Ensure that predicateMetadata implements algorithm.PredicateMetadata. | // Ensure that predicateMetadata implements algorithm.PredicateMetadata. | ||||||
| @@ -137,17 +158,24 @@ func (pfactory *PredicateMetadataFactory) GetMetadata(pod *v1.Pod, nodeNameToInf | |||||||
| 	if pod == nil { | 	if pod == nil { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  | 	// existingPodSpreadConstraintsMap represents how existing pods matches "pod" | ||||||
|  | 	// on its spread constraints | ||||||
|  | 	existingPodSpreadConstraintsMap, err := getTPMapMatchingSpreadConstraints(pod, nodeNameToInfoMap) | ||||||
|  | 	if err != nil { | ||||||
|  | 		klog.Errorf("Error calculating spreadConstraintsMap: %v", err) | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
| 	// existingPodAntiAffinityMap will be used later for efficient check on existing pods' anti-affinity | 	// existingPodAntiAffinityMap will be used later for efficient check on existing pods' anti-affinity | ||||||
| 	existingPodAntiAffinityMap, err := getTPMapMatchingExistingAntiAffinity(pod, nodeNameToInfoMap) | 	existingPodAntiAffinityMap, err := getTPMapMatchingExistingAntiAffinity(pod, nodeNameToInfoMap) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		klog.Errorf("[predicate meta data generation] error finding pods whose affinity terms are matched: %v", err) | 		klog.Errorf("Error calculating existingPodAntiAffinityMap: %v", err) | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 	// incomingPodAffinityMap will be used later for efficient check on incoming pod's affinity | 	// incomingPodAffinityMap will be used later for efficient check on incoming pod's affinity | ||||||
| 	// incomingPodAntiAffinityMap will be used later for efficient check on incoming pod's anti-affinity | 	// incomingPodAntiAffinityMap will be used later for efficient check on incoming pod's anti-affinity | ||||||
| 	incomingPodAffinityMap, incomingPodAntiAffinityMap, err := getTPMapMatchingIncomingAffinityAntiAffinity(pod, nodeNameToInfoMap) | 	incomingPodAffinityMap, incomingPodAntiAffinityMap, err := getTPMapMatchingIncomingAffinityAntiAffinity(pod, nodeNameToInfoMap) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		klog.Errorf("[predicate meta data generation] error finding pods that match affinity terms: %v", err) | 		klog.Errorf("Error calculating incomingPod(Anti)AffinityMap: %v", err) | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 	predicateMetadata := &predicateMetadata{ | 	predicateMetadata := &predicateMetadata{ | ||||||
| @@ -158,6 +186,7 @@ func (pfactory *PredicateMetadataFactory) GetMetadata(pod *v1.Pod, nodeNameToInf | |||||||
| 		topologyPairsPotentialAffinityPods:     incomingPodAffinityMap, | 		topologyPairsPotentialAffinityPods:     incomingPodAffinityMap, | ||||||
| 		topologyPairsPotentialAntiAffinityPods: incomingPodAntiAffinityMap, | 		topologyPairsPotentialAntiAffinityPods: incomingPodAntiAffinityMap, | ||||||
| 		topologyPairsAntiAffinityPodsMap:       existingPodAntiAffinityMap, | 		topologyPairsAntiAffinityPodsMap:       existingPodAntiAffinityMap, | ||||||
|  | 		topologyPairsPodSpreadMap:              existingPodSpreadConstraintsMap, | ||||||
| 	} | 	} | ||||||
| 	for predicateName, precomputeFunc := range predicateMetadataProducers { | 	for predicateName, precomputeFunc := range predicateMetadataProducers { | ||||||
| 		klog.V(10).Infof("Precompute: %v", predicateName) | 		klog.V(10).Infof("Precompute: %v", predicateName) | ||||||
| @@ -166,46 +195,224 @@ func (pfactory *PredicateMetadataFactory) GetMetadata(pod *v1.Pod, nodeNameToInf | |||||||
| 	return predicateMetadata | 	return predicateMetadata | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func getTPMapMatchingSpreadConstraints(pod *v1.Pod, nodeInfoMap map[string]*schedulernodeinfo.NodeInfo) (*topologyPairsPodSpreadMap, error) { | ||||||
|  | 	// we have feature gating in APIserver to strip the spec | ||||||
|  | 	// so don't need to re-check feature gate, just check length of constraints | ||||||
|  | 	constraints := getHardTopologySpreadConstraints(pod) | ||||||
|  | 	if len(constraints) == 0 { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	allNodeNames := make([]string, 0, len(nodeInfoMap)) | ||||||
|  | 	for name := range nodeInfoMap { | ||||||
|  | 		allNodeNames = append(allNodeNames, name) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var lock sync.Mutex | ||||||
|  | 	var firstError error | ||||||
|  |  | ||||||
|  | 	topologyPairsPodSpreadMap := newTopologyPairsPodSpreadMap() | ||||||
|  |  | ||||||
|  | 	appendTopologyPairsMaps := func(toAppend *topologyPairsMaps) { | ||||||
|  | 		lock.Lock() | ||||||
|  | 		topologyPairsPodSpreadMap.appendMaps(toAppend) | ||||||
|  | 		lock.Unlock() | ||||||
|  | 	} | ||||||
|  | 	catchError := func(err error) { | ||||||
|  | 		lock.Lock() | ||||||
|  | 		if firstError == nil { | ||||||
|  | 			firstError = err | ||||||
|  | 		} | ||||||
|  | 		lock.Unlock() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  |  | ||||||
|  | 	processNode := func(i int) { | ||||||
|  | 		nodeInfo := nodeInfoMap[allNodeNames[i]] | ||||||
|  | 		node := nodeInfo.Node() | ||||||
|  | 		if node == nil { | ||||||
|  | 			catchError(fmt.Errorf("node %q not found", allNodeNames[i])) | ||||||
|  | 			cancel() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		// Be design if NodeAffinity or NodeSelector is defined, spreading is | ||||||
|  | 		// applied to nodes that pass those filters. | ||||||
|  | 		if !podMatchesNodeSelectorAndAffinityTerms(pod, node) { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		// ensure current node's labels contains all topologyKeys in 'constraints' | ||||||
|  | 		for _, constraint := range constraints { | ||||||
|  | 			if _, ok := node.Labels[constraint.TopologyKey]; !ok { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		nodeTopologyMaps := newTopologyPairsMaps() | ||||||
|  | 		// nodeInfo.Pods() can be empty; or all pods don't fit | ||||||
|  | 		for _, existingPod := range nodeInfo.Pods() { | ||||||
|  | 			ok, err := podMatchesAllSpreadConstraints(existingPod, pod.Namespace, constraints) | ||||||
|  | 			if err != nil { | ||||||
|  | 				catchError(err) | ||||||
|  | 				cancel() | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			if ok { | ||||||
|  | 				for _, constraint := range constraints { | ||||||
|  | 					// constraint.TopologyKey is already guaranteed to be present | ||||||
|  | 					pair := topologyPair{key: constraint.TopologyKey, value: node.Labels[constraint.TopologyKey]} | ||||||
|  | 					nodeTopologyMaps.addTopologyPair(pair, existingPod) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		// If needed, append topology pair without entry of pods. | ||||||
|  | 		// For example, on node-x, there is no pod matching spread constraints | ||||||
|  | 		// but node-x should be also considered as a match (with match number 0) | ||||||
|  | 		// i.e. <node: node-x>: {} | ||||||
|  | 		for _, constraint := range constraints { | ||||||
|  | 			// constraint.TopologyKey is already guaranteed to be present | ||||||
|  | 			pair := topologyPair{ | ||||||
|  | 				key:   constraint.TopologyKey, | ||||||
|  | 				value: node.Labels[constraint.TopologyKey], | ||||||
|  | 			} | ||||||
|  | 			// addTopologyPairWithoutPods is a non-op if other pods match this pair | ||||||
|  | 			nodeTopologyMaps.addTopologyPairWithoutPods(pair) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		appendTopologyPairsMaps(nodeTopologyMaps) | ||||||
|  | 	} | ||||||
|  | 	workqueue.ParallelizeUntil(ctx, 16, len(allNodeNames), processNode) | ||||||
|  |  | ||||||
|  | 	if firstError != nil { | ||||||
|  | 		return nil, firstError | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// calculate min match for each topology pair | ||||||
|  | 	topologyPairsPodSpreadMap.minMatches = make([]int32, len(constraints)) | ||||||
|  | 	tpKeyIdx := make(map[string]int) | ||||||
|  | 	for i, constraint := range constraints { | ||||||
|  | 		tpKeyIdx[constraint.TopologyKey] = i | ||||||
|  | 		topologyPairsPodSpreadMap.minMatches[i] = MaxInt32 | ||||||
|  | 	} | ||||||
|  | 	for pair, podSet := range topologyPairsPodSpreadMap.topologyPairToPods { | ||||||
|  | 		idx := tpKeyIdx[pair.key] | ||||||
|  | 		// short circuit if we see 0 as min match of the topologyKey | ||||||
|  | 		if topologyPairsPodSpreadMap.minMatches[idx] == 0 { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if l := int32(len(podSet)); l < topologyPairsPodSpreadMap.minMatches[idx] { | ||||||
|  | 			topologyPairsPodSpreadMap.minMatches[idx] = l | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return topologyPairsPodSpreadMap, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getHardTopologySpreadConstraints(pod *v1.Pod) (constraints []v1.TopologySpreadConstraint) { | ||||||
|  | 	if pod != nil { | ||||||
|  | 		for _, constraint := range pod.Spec.TopologySpreadConstraints { | ||||||
|  | 			if constraint.WhenUnsatisfiable == v1.DoNotSchedule { | ||||||
|  | 				constraints = append(constraints, constraint) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func podMatchesAllSpreadConstraints(pod *v1.Pod, ns string, constraints []v1.TopologySpreadConstraint) (bool, error) { | ||||||
|  | 	if len(constraints) == 0 || pod.Namespace != ns { | ||||||
|  | 		return false, nil | ||||||
|  | 	} | ||||||
|  | 	return podLabelsMatchesSpreadConstraints(pod.Labels, constraints) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // some corner cases: | ||||||
|  | // 1. podLabels = nil, constraint.LabelSelector = nil => returns false | ||||||
|  | // 2. podLabels = nil => returns false | ||||||
|  | // 3. constraint.LabelSelector = nil => returns false | ||||||
|  | func podLabelsMatchesSpreadConstraints(podLabels map[string]string, constraints []v1.TopologySpreadConstraint) (bool, error) { | ||||||
|  | 	if len(constraints) == 0 { | ||||||
|  | 		return false, nil | ||||||
|  | 	} | ||||||
|  | 	for _, constraint := range constraints { | ||||||
|  | 		selector, err := metav1.LabelSelectorAsSelector(constraint.LabelSelector) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return false, err | ||||||
|  | 		} | ||||||
|  | 		if !selector.Matches(labels.Set(podLabels)) { | ||||||
|  | 			return false, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return true, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // returns a pointer to a new topologyPairsMaps | // returns a pointer to a new topologyPairsMaps | ||||||
| func newTopologyPairsMaps() *topologyPairsMaps { | func newTopologyPairsMaps() *topologyPairsMaps { | ||||||
| 	return &topologyPairsMaps{topologyPairToPods: make(map[topologyPair]podSet), | 	return &topologyPairsMaps{topologyPairToPods: make(map[topologyPair]podSet), | ||||||
| 		podToTopologyPairs: make(map[string]topologyPairSet)} | 		podToTopologyPairs: make(map[string]topologyPairSet)} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (topologyPairsMaps *topologyPairsMaps) addTopologyPair(pair topologyPair, pod *v1.Pod) { | func (m *topologyPairsMaps) addTopologyPair(pair topologyPair, pod *v1.Pod) { | ||||||
| 	podFullName := schedutil.GetPodFullName(pod) | 	podFullName := schedutil.GetPodFullName(pod) | ||||||
| 	if topologyPairsMaps.topologyPairToPods[pair] == nil { | 	if m.topologyPairToPods[pair] == nil { | ||||||
| 		topologyPairsMaps.topologyPairToPods[pair] = make(map[*v1.Pod]struct{}) | 		m.topologyPairToPods[pair] = make(map[*v1.Pod]struct{}) | ||||||
| 	} | 	} | ||||||
| 	topologyPairsMaps.topologyPairToPods[pair][pod] = struct{}{} | 	m.topologyPairToPods[pair][pod] = struct{}{} | ||||||
| 	if topologyPairsMaps.podToTopologyPairs[podFullName] == nil { | 	if m.podToTopologyPairs[podFullName] == nil { | ||||||
| 		topologyPairsMaps.podToTopologyPairs[podFullName] = make(map[topologyPair]struct{}) | 		m.podToTopologyPairs[podFullName] = make(map[topologyPair]struct{}) | ||||||
| 	} | 	} | ||||||
| 	topologyPairsMaps.podToTopologyPairs[podFullName][pair] = struct{}{} | 	m.podToTopologyPairs[podFullName][pair] = struct{}{} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (topologyPairsMaps *topologyPairsMaps) removePod(deletedPod *v1.Pod) { | // add a topology pair holder if needed | ||||||
|  | func (m *topologyPairsMaps) addTopologyPairWithoutPods(pair topologyPair) { | ||||||
|  | 	if m.topologyPairToPods[pair] == nil { | ||||||
|  | 		m.topologyPairToPods[pair] = make(map[*v1.Pod]struct{}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *topologyPairsMaps) removePod(deletedPod *v1.Pod) { | ||||||
| 	deletedPodFullName := schedutil.GetPodFullName(deletedPod) | 	deletedPodFullName := schedutil.GetPodFullName(deletedPod) | ||||||
| 	for pair := range topologyPairsMaps.podToTopologyPairs[deletedPodFullName] { | 	for pair := range m.podToTopologyPairs[deletedPodFullName] { | ||||||
| 		delete(topologyPairsMaps.topologyPairToPods[pair], deletedPod) | 		delete(m.topologyPairToPods[pair], deletedPod) | ||||||
| 		if len(topologyPairsMaps.topologyPairToPods[pair]) == 0 { | 		if len(m.topologyPairToPods[pair]) == 0 { | ||||||
| 			delete(topologyPairsMaps.topologyPairToPods, pair) | 			delete(m.topologyPairToPods, pair) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	delete(topologyPairsMaps.podToTopologyPairs, deletedPodFullName) | 	delete(m.podToTopologyPairs, deletedPodFullName) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (topologyPairsMaps *topologyPairsMaps) appendMaps(toAppend *topologyPairsMaps) { | func (m *topologyPairsMaps) appendMaps(toAppend *topologyPairsMaps) { | ||||||
| 	if toAppend == nil { | 	if toAppend == nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	for pair := range toAppend.topologyPairToPods { | 	for pair := range toAppend.topologyPairToPods { | ||||||
| 		for pod := range toAppend.topologyPairToPods[pair] { | 		if podSet := toAppend.topologyPairToPods[pair]; len(podSet) == 0 { | ||||||
| 			topologyPairsMaps.addTopologyPair(pair, pod) | 			m.addTopologyPairWithoutPods(pair) | ||||||
|  | 		} else { | ||||||
|  | 			for pod := range podSet { | ||||||
|  | 				m.addTopologyPair(pair, pod) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (m *topologyPairsMaps) clone() *topologyPairsMaps { | ||||||
|  | 	copy := newTopologyPairsMaps() | ||||||
|  | 	copy.appendMaps(m) | ||||||
|  | 	return copy | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (podSpreadMap *topologyPairsPodSpreadMap) clone() *topologyPairsPodSpreadMap { | ||||||
|  | 	// podSpreadMap could be nil when EvenPodsSpread feature is disabled | ||||||
|  | 	if podSpreadMap == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	copy := newTopologyPairsPodSpreadMap() | ||||||
|  | 	copy.minMatches = append([]int32(nil), podSpreadMap.minMatches...) | ||||||
|  | 	copy.topologyPairsMaps.appendMaps(podSpreadMap.topologyPairsMaps) | ||||||
|  | 	return copy | ||||||
|  | } | ||||||
|  |  | ||||||
| // RemovePod changes predicateMetadata assuming that the given `deletedPod` is | // RemovePod changes predicateMetadata assuming that the given `deletedPod` is | ||||||
| // deleted from the system. | // deleted from the system. | ||||||
| func (meta *predicateMetadata) RemovePod(deletedPod *v1.Pod) error { | func (meta *predicateMetadata) RemovePod(deletedPod *v1.Pod) error { | ||||||
| @@ -301,12 +508,10 @@ func (meta *predicateMetadata) ShallowCopy() PredicateMetadata { | |||||||
| 		ignoredExtendedResources: meta.ignoredExtendedResources, | 		ignoredExtendedResources: meta.ignoredExtendedResources, | ||||||
| 	} | 	} | ||||||
| 	newPredMeta.podPorts = append([]*v1.ContainerPort(nil), meta.podPorts...) | 	newPredMeta.podPorts = append([]*v1.ContainerPort(nil), meta.podPorts...) | ||||||
| 	newPredMeta.topologyPairsPotentialAffinityPods = newTopologyPairsMaps() | 	newPredMeta.topologyPairsPotentialAffinityPods = meta.topologyPairsPotentialAffinityPods.clone() | ||||||
| 	newPredMeta.topologyPairsPotentialAffinityPods.appendMaps(meta.topologyPairsPotentialAffinityPods) | 	newPredMeta.topologyPairsPotentialAntiAffinityPods = meta.topologyPairsPotentialAntiAffinityPods.clone() | ||||||
| 	newPredMeta.topologyPairsPotentialAntiAffinityPods = newTopologyPairsMaps() | 	newPredMeta.topologyPairsAntiAffinityPodsMap = meta.topologyPairsAntiAffinityPodsMap.clone() | ||||||
| 	newPredMeta.topologyPairsPotentialAntiAffinityPods.appendMaps(meta.topologyPairsPotentialAntiAffinityPods) | 	newPredMeta.topologyPairsPodSpreadMap = meta.topologyPairsPodSpreadMap.clone() | ||||||
| 	newPredMeta.topologyPairsAntiAffinityPodsMap = newTopologyPairsMaps() |  | ||||||
| 	newPredMeta.topologyPairsAntiAffinityPodsMap.appendMaps(meta.topologyPairsAntiAffinityPodsMap) |  | ||||||
| 	newPredMeta.serviceAffinityMatchingPodServices = append([]*v1.Service(nil), | 	newPredMeta.serviceAffinityMatchingPodServices = append([]*v1.Service(nil), | ||||||
| 		meta.serviceAffinityMatchingPodServices...) | 		meta.serviceAffinityMatchingPodServices...) | ||||||
| 	newPredMeta.serviceAffinityMatchingPodList = append([]*v1.Pod(nil), | 	newPredMeta.serviceAffinityMatchingPodList = append([]*v1.Pod(nil), | ||||||
|   | |||||||
| @@ -511,6 +511,39 @@ func TestPredicateMetadata_ShallowCopy(t *testing.T) { | |||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
|  | 		topologyPairsPodSpreadMap: &topologyPairsPodSpreadMap{ | ||||||
|  | 			minMatches: []int32{1}, | ||||||
|  | 			topologyPairsMaps: &topologyPairsMaps{ | ||||||
|  | 				topologyPairToPods: map[topologyPair]podSet{ | ||||||
|  | 					{key: "name", value: "nodeA"}: { | ||||||
|  | 						&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p1", Labels: selector1}, | ||||||
|  | 							Spec: v1.PodSpec{NodeName: "nodeA"}, | ||||||
|  | 						}: struct{}{}, | ||||||
|  | 					}, | ||||||
|  | 					{key: "name", value: "nodeC"}: { | ||||||
|  | 						&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p2"}, | ||||||
|  | 							Spec: v1.PodSpec{ | ||||||
|  | 								NodeName: "nodeC", | ||||||
|  | 							}, | ||||||
|  | 						}: struct{}{}, | ||||||
|  | 						&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p6", Labels: selector1}, | ||||||
|  | 							Spec: v1.PodSpec{NodeName: "nodeC"}, | ||||||
|  | 						}: struct{}{}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				podToTopologyPairs: map[string]topologyPairSet{ | ||||||
|  | 					"p1_": { | ||||||
|  | 						topologyPair{key: "name", value: "nodeA"}: struct{}{}, | ||||||
|  | 					}, | ||||||
|  | 					"p2_": { | ||||||
|  | 						topologyPair{key: "name", value: "nodeC"}: struct{}{}, | ||||||
|  | 					}, | ||||||
|  | 					"p6_": { | ||||||
|  | 						topologyPair{key: "name", value: "nodeC"}: struct{}{}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
| 		serviceAffinityInUse: true, | 		serviceAffinityInUse: true, | ||||||
| 		serviceAffinityMatchingPodList: []*v1.Pod{ | 		serviceAffinityMatchingPodList: []*v1.Pod{ | ||||||
| 			{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}}, | 			{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}}, | ||||||
| @@ -791,3 +824,409 @@ func TestGetTPMapMatchingIncomingAffinityAntiAffinity(t *testing.T) { | |||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestPodLabelsMatchesSpreadConstraints(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name        string | ||||||
|  | 		podLabels   map[string]string | ||||||
|  | 		constraints []v1.TopologySpreadConstraint | ||||||
|  | 		want        bool | ||||||
|  | 		wantErr     bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name:      "normal match", | ||||||
|  | 			podLabels: map[string]string{"foo": "", "bar": ""}, | ||||||
|  | 			constraints: []v1.TopologySpreadConstraint{ | ||||||
|  | 				{ | ||||||
|  | 					LabelSelector: &metav1.LabelSelector{ | ||||||
|  | 						MatchExpressions: []metav1.LabelSelectorRequirement{ | ||||||
|  | 							{ | ||||||
|  | 								Key:      "foo", | ||||||
|  | 								Operator: metav1.LabelSelectorOpExists, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			want: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:      "normal mismatch", | ||||||
|  | 			podLabels: map[string]string{"foo": "", "baz": ""}, | ||||||
|  | 			constraints: []v1.TopologySpreadConstraint{ | ||||||
|  | 				{ | ||||||
|  | 					LabelSelector: &metav1.LabelSelector{ | ||||||
|  | 						MatchExpressions: []metav1.LabelSelectorRequirement{ | ||||||
|  | 							{ | ||||||
|  | 								Key:      "foo", | ||||||
|  | 								Operator: metav1.LabelSelectorOpExists, | ||||||
|  | 							}, | ||||||
|  | 							{ | ||||||
|  | 								Key:      "bar", | ||||||
|  | 								Operator: metav1.LabelSelectorOpExists, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			want: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "podLabels is nil", | ||||||
|  | 			constraints: []v1.TopologySpreadConstraint{ | ||||||
|  | 				{ | ||||||
|  | 					LabelSelector: &metav1.LabelSelector{ | ||||||
|  | 						MatchExpressions: []metav1.LabelSelectorRequirement{ | ||||||
|  | 							{ | ||||||
|  | 								Key:      "foo", | ||||||
|  | 								Operator: metav1.LabelSelectorOpExists, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			want: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "constraint.LabelSelector is nil", | ||||||
|  | 			podLabels: map[string]string{ | ||||||
|  | 				"foo": "", | ||||||
|  | 				"bar": "", | ||||||
|  | 			}, | ||||||
|  | 			constraints: []v1.TopologySpreadConstraint{ | ||||||
|  | 				{ | ||||||
|  | 					MaxSkew: 1, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			want: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "both podLabels and constraint.LabelSelector are nil", | ||||||
|  | 			constraints: []v1.TopologySpreadConstraint{ | ||||||
|  | 				{ | ||||||
|  | 					MaxSkew: 1, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			want: false, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			got, err := podLabelsMatchesSpreadConstraints(tt.podLabels, tt.constraints) | ||||||
|  | 			if (err != nil) != tt.wantErr { | ||||||
|  | 				t.Errorf("podLabelsMatchesSpreadConstraints() error = %v, wantErr %v", err, tt.wantErr) | ||||||
|  | 			} | ||||||
|  | 			if got != tt.want { | ||||||
|  | 				t.Errorf("podLabelsMatchesSpreadConstraints() = %v, want %v", got, tt.want) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestGetTPMapMatchingSpreadConstraints(t *testing.T) { | ||||||
|  | 	// we need to inject the exact pod pointers to want.topologyPairsMaps.topologyPairToPods | ||||||
|  | 	// otherwise, *pod (as key of a map) will always fail in reflect.DeepEqual() | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name              string | ||||||
|  | 		pod               *v1.Pod | ||||||
|  | 		nodes             []*v1.Node | ||||||
|  | 		existingPods      []*v1.Pod | ||||||
|  | 		injectPodPointers map[topologyPair][]int | ||||||
|  | 		want              *topologyPairsPodSpreadMap | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "clean cluster with one spreadConstraint", | ||||||
|  | 			pod: makePod().name("p").label("foo", "").spreadConstraint( | ||||||
|  | 				1, "zone", hardSpread, makeLabelSelector().exists("foo").obj(), | ||||||
|  | 			).obj(), | ||||||
|  | 			nodes: []*v1.Node{ | ||||||
|  | 				makeNode().name("node-a").label("zone", "zone1").label("node", "node-a").obj(), | ||||||
|  | 				makeNode().name("node-b").label("zone", "zone1").label("node", "node-b").obj(), | ||||||
|  | 				makeNode().name("node-x").label("zone", "zone2").label("node", "node-x").obj(), | ||||||
|  | 				makeNode().name("node-y").label("zone", "zone2").label("node", "node-y").obj(), | ||||||
|  | 			}, | ||||||
|  | 			injectPodPointers: map[topologyPair][]int{ | ||||||
|  | 				// denotes no existing pod is matched on this zone pair, but still needed to be | ||||||
|  | 				// calculated if incoming pod matches its own spread constraints | ||||||
|  | 				{key: "zone", value: "zone1"}: []int{}, | ||||||
|  | 				{key: "zone", value: "zone2"}: []int{}, | ||||||
|  | 			}, | ||||||
|  | 			want: &topologyPairsPodSpreadMap{ | ||||||
|  | 				minMatches: []int32{0}, | ||||||
|  | 				topologyPairsMaps: &topologyPairsMaps{ | ||||||
|  | 					podToTopologyPairs: make(map[string]topologyPairSet), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "normal case with one spreadConstraint", | ||||||
|  | 			pod: makePod().name("p").label("foo", "").spreadConstraint( | ||||||
|  | 				1, "zone", hardSpread, makeLabelSelector().exists("foo").obj(), | ||||||
|  | 			).obj(), | ||||||
|  | 			nodes: []*v1.Node{ | ||||||
|  | 				makeNode().name("node-a").label("zone", "zone1").label("node", "node-a").obj(), | ||||||
|  | 				makeNode().name("node-b").label("zone", "zone1").label("node", "node-b").obj(), | ||||||
|  | 				makeNode().name("node-x").label("zone", "zone2").label("node", "node-x").obj(), | ||||||
|  | 				makeNode().name("node-y").label("zone", "zone2").label("node", "node-y").obj(), | ||||||
|  | 			}, | ||||||
|  | 			existingPods: []*v1.Pod{ | ||||||
|  | 				makePod().name("p-a1").node("node-a").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-a2").node("node-a").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-b1").node("node-b").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y1").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y2").node("node-y").label("foo", "").obj(), | ||||||
|  | 			}, | ||||||
|  | 			injectPodPointers: map[topologyPair][]int{ | ||||||
|  | 				// denotes existingPods[0,1,2] | ||||||
|  | 				{key: "zone", value: "zone1"}: []int{0, 1, 2}, | ||||||
|  | 				// denotes existingPods[3,4] | ||||||
|  | 				{key: "zone", value: "zone2"}: []int{3, 4}, | ||||||
|  | 			}, | ||||||
|  | 			want: &topologyPairsPodSpreadMap{ | ||||||
|  | 				minMatches: []int32{2}, | ||||||
|  | 				topologyPairsMaps: &topologyPairsMaps{ | ||||||
|  | 					podToTopologyPairs: map[string]topologyPairSet{ | ||||||
|  | 						"p-a1_": newPairSet("zone", "zone1"), | ||||||
|  | 						"p-a2_": newPairSet("zone", "zone1"), | ||||||
|  | 						"p-b1_": newPairSet("zone", "zone1"), | ||||||
|  | 						"p-y1_": newPairSet("zone", "zone2"), | ||||||
|  | 						"p-y2_": newPairSet("zone", "zone2"), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "namespace mis-match doesn't count", | ||||||
|  | 			pod: makePod().name("p").label("foo", "").spreadConstraint( | ||||||
|  | 				1, "zone", hardSpread, makeLabelSelector().exists("foo").obj(), | ||||||
|  | 			).obj(), | ||||||
|  | 			nodes: []*v1.Node{ | ||||||
|  | 				makeNode().name("node-a").label("zone", "zone1").label("node", "node-a").obj(), | ||||||
|  | 				makeNode().name("node-b").label("zone", "zone1").label("node", "node-b").obj(), | ||||||
|  | 				makeNode().name("node-x").label("zone", "zone2").label("node", "node-x").obj(), | ||||||
|  | 				makeNode().name("node-y").label("zone", "zone2").label("node", "node-y").obj(), | ||||||
|  | 			}, | ||||||
|  | 			existingPods: []*v1.Pod{ | ||||||
|  | 				makePod().name("p-a1").node("node-a").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-a2").namespace("ns1").node("node-a").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-b1").node("node-b").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y1").namespace("ns2").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y2").node("node-y").label("foo", "").obj(), | ||||||
|  | 			}, | ||||||
|  | 			injectPodPointers: map[topologyPair][]int{ | ||||||
|  | 				{key: "zone", value: "zone1"}: []int{0, 2}, | ||||||
|  | 				{key: "zone", value: "zone2"}: []int{4}, | ||||||
|  | 			}, | ||||||
|  | 			want: &topologyPairsPodSpreadMap{ | ||||||
|  | 				minMatches: []int32{1}, | ||||||
|  | 				topologyPairsMaps: &topologyPairsMaps{ | ||||||
|  | 					podToTopologyPairs: map[string]topologyPairSet{ | ||||||
|  | 						"p-a1_": newPairSet("zone", "zone1"), | ||||||
|  | 						"p-b1_": newPairSet("zone", "zone1"), | ||||||
|  | 						"p-y2_": newPairSet("zone", "zone2"), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "normal case with two spreadConstraints", | ||||||
|  | 			pod: makePod().name("p").label("foo", ""). | ||||||
|  | 				spreadConstraint(1, "zone", hardSpread, makeLabelSelector().exists("foo").obj()). | ||||||
|  | 				spreadConstraint(1, "node", hardSpread, makeLabelSelector().exists("foo").obj()). | ||||||
|  | 				obj(), | ||||||
|  | 			nodes: []*v1.Node{ | ||||||
|  | 				makeNode().name("node-a").label("zone", "zone1").label("node", "node-a").obj(), | ||||||
|  | 				makeNode().name("node-b").label("zone", "zone1").label("node", "node-b").obj(), | ||||||
|  | 				makeNode().name("node-x").label("zone", "zone2").label("node", "node-x").obj(), | ||||||
|  | 				makeNode().name("node-y").label("zone", "zone2").label("node", "node-y").obj(), | ||||||
|  | 			}, | ||||||
|  | 			existingPods: []*v1.Pod{ | ||||||
|  | 				makePod().name("p-a1").node("node-a").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-a2").node("node-a").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-b1").node("node-b").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y1").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y2").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y3").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y4").node("node-y").label("foo", "").obj(), | ||||||
|  | 			}, | ||||||
|  | 			injectPodPointers: map[topologyPair][]int{ | ||||||
|  | 				{key: "zone", value: "zone1"}:  []int{0, 1, 2}, | ||||||
|  | 				{key: "zone", value: "zone2"}:  []int{3, 4, 5, 6}, | ||||||
|  | 				{key: "node", value: "node-a"}: []int{0, 1}, | ||||||
|  | 				{key: "node", value: "node-b"}: []int{2}, | ||||||
|  | 				{key: "node", value: "node-x"}: []int{}, | ||||||
|  | 				{key: "node", value: "node-y"}: []int{3, 4, 5, 6}, | ||||||
|  | 			}, | ||||||
|  | 			want: &topologyPairsPodSpreadMap{ | ||||||
|  | 				minMatches: []int32{3, 0}, | ||||||
|  | 				topologyPairsMaps: &topologyPairsMaps{ | ||||||
|  | 					podToTopologyPairs: map[string]topologyPairSet{ | ||||||
|  | 						"p-a1_": newPairSet("zone", "zone1", "node", "node-a"), | ||||||
|  | 						"p-a2_": newPairSet("zone", "zone1", "node", "node-a"), | ||||||
|  | 						"p-b1_": newPairSet("zone", "zone1", "node", "node-b"), | ||||||
|  | 						"p-y1_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 						"p-y2_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 						"p-y3_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 						"p-y4_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "soft spreadConstraints should be bypassed", | ||||||
|  | 			pod: makePod().name("p").label("foo", ""). | ||||||
|  | 				spreadConstraint(1, "zone", softSpread, makeLabelSelector().exists("foo").obj()). | ||||||
|  | 				spreadConstraint(1, "zone", hardSpread, makeLabelSelector().exists("foo").obj()). | ||||||
|  | 				spreadConstraint(1, "zone", softSpread, makeLabelSelector().exists("foo").obj()). | ||||||
|  | 				spreadConstraint(1, "node", hardSpread, makeLabelSelector().exists("foo").obj()). | ||||||
|  | 				obj(), | ||||||
|  | 			nodes: []*v1.Node{ | ||||||
|  | 				makeNode().name("node-a").label("zone", "zone1").label("node", "node-a").obj(), | ||||||
|  | 				makeNode().name("node-b").label("zone", "zone1").label("node", "node-b").obj(), | ||||||
|  | 				makeNode().name("node-y").label("zone", "zone2").label("node", "node-y").obj(), | ||||||
|  | 			}, | ||||||
|  | 			existingPods: []*v1.Pod{ | ||||||
|  | 				makePod().name("p-a1").node("node-a").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-a2").node("node-a").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-b1").node("node-b").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y1").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y2").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y3").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y4").node("node-y").label("foo", "").obj(), | ||||||
|  | 			}, | ||||||
|  | 			injectPodPointers: map[topologyPair][]int{ | ||||||
|  | 				{key: "zone", value: "zone1"}:  []int{0, 1, 2}, | ||||||
|  | 				{key: "zone", value: "zone2"}:  []int{3, 4, 5, 6}, | ||||||
|  | 				{key: "node", value: "node-a"}: []int{0, 1}, | ||||||
|  | 				{key: "node", value: "node-b"}: []int{2}, | ||||||
|  | 				{key: "node", value: "node-y"}: []int{3, 4, 5, 6}, | ||||||
|  | 			}, | ||||||
|  | 			want: &topologyPairsPodSpreadMap{ | ||||||
|  | 				minMatches: []int32{3, 1}, | ||||||
|  | 				topologyPairsMaps: &topologyPairsMaps{ | ||||||
|  | 					podToTopologyPairs: map[string]topologyPairSet{ | ||||||
|  | 						"p-a1_": newPairSet("zone", "zone1", "node", "node-a"), | ||||||
|  | 						"p-a2_": newPairSet("zone", "zone1", "node", "node-a"), | ||||||
|  | 						"p-b1_": newPairSet("zone", "zone1", "node", "node-b"), | ||||||
|  | 						"p-y1_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 						"p-y2_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 						"p-y3_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 						"p-y4_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "different labelSelectors", | ||||||
|  | 			pod: makePod().name("p").label("foo", "").label("bar", ""). | ||||||
|  | 				spreadConstraint(1, "zone", hardSpread, makeLabelSelector().exists("foo").obj()). | ||||||
|  | 				spreadConstraint(1, "node", hardSpread, makeLabelSelector().exists("bar").obj()). | ||||||
|  | 				obj(), | ||||||
|  | 			nodes: []*v1.Node{ | ||||||
|  | 				makeNode().name("node-a").label("zone", "zone1").label("node", "node-a").obj(), | ||||||
|  | 				makeNode().name("node-b").label("zone", "zone1").label("node", "node-b").obj(), | ||||||
|  | 				makeNode().name("node-y").label("zone", "zone2").label("node", "node-y").obj(), | ||||||
|  | 			}, | ||||||
|  | 			existingPods: []*v1.Pod{ | ||||||
|  | 				makePod().name("p-a1").node("node-a").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-a2").node("node-a").label("foo", "").label("bar", "").obj(), | ||||||
|  | 				makePod().name("p-b1").node("node-b").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y1").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y2").node("node-y").label("foo", "").label("bar", "").obj(), | ||||||
|  | 				makePod().name("p-y3").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y4").node("node-y").label("foo", "").label("bar", "").obj(), | ||||||
|  | 			}, | ||||||
|  | 			injectPodPointers: map[topologyPair][]int{ | ||||||
|  | 				{key: "zone", value: "zone1"}:  []int{1}, | ||||||
|  | 				{key: "zone", value: "zone2"}:  []int{4, 6}, | ||||||
|  | 				{key: "node", value: "node-a"}: []int{1}, | ||||||
|  | 				{key: "node", value: "node-b"}: []int{}, | ||||||
|  | 				{key: "node", value: "node-y"}: []int{4, 6}, | ||||||
|  | 			}, | ||||||
|  | 			want: &topologyPairsPodSpreadMap{ | ||||||
|  | 				minMatches: []int32{1, 0}, | ||||||
|  | 				topologyPairsMaps: &topologyPairsMaps{ | ||||||
|  | 					podToTopologyPairs: map[string]topologyPairSet{ | ||||||
|  | 						"p-a2_": newPairSet("zone", "zone1", "node", "node-a"), | ||||||
|  | 						"p-y2_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 						"p-y4_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "two spreadConstraints, and with podAffinity", | ||||||
|  | 			pod: makePod().name("p").label("foo", ""). | ||||||
|  | 				nodeAffinityIn("node", []string{"node-a", "node-b", "node-y"}). // exclude node-x | ||||||
|  | 				spreadConstraint(1, "zone", hardSpread, makeLabelSelector().exists("foo").obj()). | ||||||
|  | 				spreadConstraint(1, "node", hardSpread, makeLabelSelector().exists("foo").obj()). | ||||||
|  | 				obj(), | ||||||
|  | 			nodes: []*v1.Node{ | ||||||
|  | 				makeNode().name("node-a").label("zone", "zone1").label("node", "node-a").obj(), | ||||||
|  | 				makeNode().name("node-b").label("zone", "zone1").label("node", "node-b").obj(), | ||||||
|  | 				makeNode().name("node-x").label("zone", "zone2").label("node", "node-x").obj(), | ||||||
|  | 				makeNode().name("node-y").label("zone", "zone2").label("node", "node-y").obj(), | ||||||
|  | 			}, | ||||||
|  | 			existingPods: []*v1.Pod{ | ||||||
|  | 				makePod().name("p-a1").node("node-a").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-a2").node("node-a").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-b1").node("node-b").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y1").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y2").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y3").node("node-y").label("foo", "").obj(), | ||||||
|  | 				makePod().name("p-y4").node("node-y").label("foo", "").obj(), | ||||||
|  | 			}, | ||||||
|  | 			injectPodPointers: map[topologyPair][]int{ | ||||||
|  | 				{key: "zone", value: "zone1"}:  []int{0, 1, 2}, | ||||||
|  | 				{key: "zone", value: "zone2"}:  []int{3, 4, 5, 6}, | ||||||
|  | 				{key: "node", value: "node-a"}: []int{0, 1}, | ||||||
|  | 				{key: "node", value: "node-b"}: []int{2}, | ||||||
|  | 				{key: "node", value: "node-y"}: []int{3, 4, 5, 6}, | ||||||
|  | 			}, | ||||||
|  | 			want: &topologyPairsPodSpreadMap{ | ||||||
|  | 				minMatches: []int32{3, 1}, | ||||||
|  | 				topologyPairsMaps: &topologyPairsMaps{ | ||||||
|  | 					podToTopologyPairs: map[string]topologyPairSet{ | ||||||
|  | 						"p-a1_": newPairSet("zone", "zone1", "node", "node-a"), | ||||||
|  | 						"p-a2_": newPairSet("zone", "zone1", "node", "node-a"), | ||||||
|  | 						"p-b1_": newPairSet("zone", "zone1", "node", "node-b"), | ||||||
|  | 						"p-y1_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 						"p-y2_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 						"p-y3_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 						"p-y4_": newPairSet("zone", "zone2", "node", "node-y"), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			tt.want.topologyPairToPods = make(map[topologyPair]podSet) | ||||||
|  | 			for pair, indexes := range tt.injectPodPointers { | ||||||
|  | 				pSet := make(podSet) | ||||||
|  | 				for _, i := range indexes { | ||||||
|  | 					pSet[tt.existingPods[i]] = struct{}{} | ||||||
|  | 				} | ||||||
|  | 				tt.want.topologyPairToPods[pair] = pSet | ||||||
|  | 			} | ||||||
|  | 			nodeInfoMap := schedulernodeinfo.CreateNodeNameToInfoMap(tt.existingPods, tt.nodes) | ||||||
|  | 			if got, _ := getTPMapMatchingSpreadConstraints(tt.pod, nodeInfoMap); !reflect.DeepEqual(got, tt.want) { | ||||||
|  | 				t.Errorf("getTPMapMatchingSpreadConstraints() = %v, want %v", got, tt.want) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	hardSpread = v1.DoNotSchedule | ||||||
|  | 	softSpread = v1.ScheduleAnyway | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func newPairSet(kv ...string) topologyPairSet { | ||||||
|  | 	result := make(topologyPairSet) | ||||||
|  | 	for i := 0; i < len(kv); i += 2 { | ||||||
|  | 		pair := topologyPair{key: kv[i], value: kv[i+1]} | ||||||
|  | 		result[pair] = struct{}{} | ||||||
|  | 	} | ||||||
|  | 	return result | ||||||
|  | } | ||||||
|   | |||||||
| @@ -19,8 +19,9 @@ package predicates | |||||||
| import ( | import ( | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	v1 "k8s.io/api/core/v1" | 	"k8s.io/api/core/v1" | ||||||
| 	storagev1beta1 "k8s.io/api/storage/v1beta1" | 	storagev1beta1 "k8s.io/api/storage/v1beta1" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/labels" | 	"k8s.io/apimachinery/pkg/labels" | ||||||
| 	"k8s.io/apimachinery/pkg/util/sets" | 	"k8s.io/apimachinery/pkg/util/sets" | ||||||
| 	utilfeature "k8s.io/apiserver/pkg/util/feature" | 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||||
| @@ -147,3 +148,169 @@ func isCSIMigrationOn(csiNode *storagev1beta1.CSINode, pluginName string) bool { | |||||||
|  |  | ||||||
| 	return mpaSet.Has(pluginName) | 	return mpaSet.Has(pluginName) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // utilities for building pod/node objects using a "chained" manner | ||||||
|  | type nodeSelectorWrapper struct{ v1.NodeSelector } | ||||||
|  |  | ||||||
|  | func makeNodeSelector() *nodeSelectorWrapper { | ||||||
|  | 	return &nodeSelectorWrapper{v1.NodeSelector{}} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NOTE: each time we append a selectorTerm into `s` | ||||||
|  | // and overall all selecterTerms are ORed | ||||||
|  | func (s *nodeSelectorWrapper) in(key string, vals []string) *nodeSelectorWrapper { | ||||||
|  | 	expression := v1.NodeSelectorRequirement{ | ||||||
|  | 		Key:      key, | ||||||
|  | 		Operator: v1.NodeSelectorOpIn, | ||||||
|  | 		Values:   vals, | ||||||
|  | 	} | ||||||
|  | 	selectorTerm := v1.NodeSelectorTerm{} | ||||||
|  | 	selectorTerm.MatchExpressions = append(selectorTerm.MatchExpressions, expression) | ||||||
|  | 	s.NodeSelectorTerms = append(s.NodeSelectorTerms, selectorTerm) | ||||||
|  | 	return s | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *nodeSelectorWrapper) obj() *v1.NodeSelector { | ||||||
|  | 	return &s.NodeSelector | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type labelSelectorWrapper struct{ metav1.LabelSelector } | ||||||
|  |  | ||||||
|  | func makeLabelSelector() *labelSelectorWrapper { | ||||||
|  | 	return &labelSelectorWrapper{metav1.LabelSelector{}} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *labelSelectorWrapper) label(k, v string) *labelSelectorWrapper { | ||||||
|  | 	if s.MatchLabels == nil { | ||||||
|  | 		s.MatchLabels = make(map[string]string) | ||||||
|  | 	} | ||||||
|  | 	s.MatchLabels[k] = v | ||||||
|  | 	return s | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *labelSelectorWrapper) in(key string, vals []string) *labelSelectorWrapper { | ||||||
|  | 	expression := metav1.LabelSelectorRequirement{ | ||||||
|  | 		Key:      key, | ||||||
|  | 		Operator: metav1.LabelSelectorOpIn, | ||||||
|  | 		Values:   vals, | ||||||
|  | 	} | ||||||
|  | 	s.MatchExpressions = append(s.MatchExpressions, expression) | ||||||
|  | 	return s | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *labelSelectorWrapper) notIn(key string, vals []string) *labelSelectorWrapper { | ||||||
|  | 	expression := metav1.LabelSelectorRequirement{ | ||||||
|  | 		Key:      key, | ||||||
|  | 		Operator: metav1.LabelSelectorOpNotIn, | ||||||
|  | 		Values:   vals, | ||||||
|  | 	} | ||||||
|  | 	s.MatchExpressions = append(s.MatchExpressions, expression) | ||||||
|  | 	return s | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *labelSelectorWrapper) exists(k string) *labelSelectorWrapper { | ||||||
|  | 	expression := metav1.LabelSelectorRequirement{ | ||||||
|  | 		Key:      k, | ||||||
|  | 		Operator: metav1.LabelSelectorOpExists, | ||||||
|  | 	} | ||||||
|  | 	s.MatchExpressions = append(s.MatchExpressions, expression) | ||||||
|  | 	return s | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *labelSelectorWrapper) notExist(k string) *labelSelectorWrapper { | ||||||
|  | 	expression := metav1.LabelSelectorRequirement{ | ||||||
|  | 		Key:      k, | ||||||
|  | 		Operator: metav1.LabelSelectorOpDoesNotExist, | ||||||
|  | 	} | ||||||
|  | 	s.MatchExpressions = append(s.MatchExpressions, expression) | ||||||
|  | 	return s | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *labelSelectorWrapper) obj() *metav1.LabelSelector { | ||||||
|  | 	return &s.LabelSelector | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type podWrapper struct{ v1.Pod } | ||||||
|  |  | ||||||
|  | func makePod() *podWrapper { | ||||||
|  | 	return &podWrapper{v1.Pod{}} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *podWrapper) obj() *v1.Pod { | ||||||
|  | 	return &p.Pod | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *podWrapper) name(s string) *podWrapper { | ||||||
|  | 	p.Name = s | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *podWrapper) namespace(s string) *podWrapper { | ||||||
|  | 	p.Namespace = s | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *podWrapper) node(s string) *podWrapper { | ||||||
|  | 	p.Spec.NodeName = s | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *podWrapper) nodeSelector(m map[string]string) *podWrapper { | ||||||
|  | 	p.Spec.NodeSelector = m | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // particular represents HARD node affinity | ||||||
|  | func (p *podWrapper) nodeAffinityIn(key string, vals []string) *podWrapper { | ||||||
|  | 	if p.Spec.Affinity == nil { | ||||||
|  | 		p.Spec.Affinity = &v1.Affinity{} | ||||||
|  | 	} | ||||||
|  | 	if p.Spec.Affinity.NodeAffinity == nil { | ||||||
|  | 		p.Spec.Affinity.NodeAffinity = &v1.NodeAffinity{} | ||||||
|  | 	} | ||||||
|  | 	nodeSelector := makeNodeSelector().in(key, vals).obj() | ||||||
|  | 	p.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution = nodeSelector | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *podWrapper) spreadConstraint(maxSkew int, tpKey string, mode v1.UnsatisfiableConstraintResponse, selector *metav1.LabelSelector) *podWrapper { | ||||||
|  | 	c := v1.TopologySpreadConstraint{ | ||||||
|  | 		MaxSkew:           int32(maxSkew), | ||||||
|  | 		TopologyKey:       tpKey, | ||||||
|  | 		WhenUnsatisfiable: mode, | ||||||
|  | 		LabelSelector:     selector, | ||||||
|  | 	} | ||||||
|  | 	p.Spec.TopologySpreadConstraints = append(p.Spec.TopologySpreadConstraints, c) | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *podWrapper) label(k, v string) *podWrapper { | ||||||
|  | 	if p.Labels == nil { | ||||||
|  | 		p.Labels = make(map[string]string) | ||||||
|  | 	} | ||||||
|  | 	p.Labels[k] = v | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type nodeWrapper struct{ v1.Node } | ||||||
|  |  | ||||||
|  | func makeNode() *nodeWrapper { | ||||||
|  | 	return &nodeWrapper{v1.Node{}} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n *nodeWrapper) obj() *v1.Node { | ||||||
|  | 	return &n.Node | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n *nodeWrapper) name(s string) *nodeWrapper { | ||||||
|  | 	n.Name = s | ||||||
|  | 	return n | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n *nodeWrapper) label(k, v string) *nodeWrapper { | ||||||
|  | 	if n.Labels == nil { | ||||||
|  | 		n.Labels = make(map[string]string) | ||||||
|  | 	} | ||||||
|  | 	n.Labels[k] = v | ||||||
|  | 	return n | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Wei Huang
					Wei Huang