diff --git a/controller/volume_controller.go b/controller/volume_controller.go index c5301b27fd..db2290550f 100644 --- a/controller/volume_controller.go +++ b/controller/volume_controller.go @@ -2304,10 +2304,15 @@ func (c *VolumeController) listReadySchedulableAndScheduledNodes(volume *longhor return nil, err } + tolerateEmptyNodeSelectorVolume, err := c.ds.GetSettingAsBool(types.SettingNameTolerateEmptyNodeSelectorVolume) + if err != nil { + logrus.Errorf("Failed to get tolerate empty node selector volume setting: %v", err) + } + filteredReadyNodes := readyNodes if len(volume.Spec.NodeSelector) != 0 { for nodeName, node := range readyNodes { - if !types.IsSelectorsInTags(node.Spec.Tags, volume.Spec.NodeSelector) { + if !types.IsSelectorsInTags(node.Spec.Tags, volume.Spec.NodeSelector, tolerateEmptyNodeSelectorVolume) { delete(filteredReadyNodes, nodeName) } } diff --git a/scheduler/replica_scheduler.go b/scheduler/replica_scheduler.go index 6444892151..04b2f3bc50 100644 --- a/scheduler/replica_scheduler.go +++ b/scheduler/replica_scheduler.go @@ -147,7 +147,7 @@ func (rcs *ReplicaScheduler) getDiskCandidates(nodeInfo map[string]*longhorn.Nod nodeSoftAntiAffinity, err := rcs.ds.GetSettingAsBool(types.SettingNameReplicaSoftAntiAffinity) if err != nil { - logrus.Errorf("error getting replica soft anti-affinity setting: %v", err) + logrus.Errorf("Failed to get replica soft anti-affinity setting: %v", err) } if volume.Spec.ReplicaSoftAntiAffinity != longhorn.ReplicaSoftAntiAffinityDefault && @@ -157,7 +157,7 @@ func (rcs *ReplicaScheduler) getDiskCandidates(nodeInfo map[string]*longhorn.Nod zoneSoftAntiAffinity, err := rcs.ds.GetSettingAsBool(types.SettingNameReplicaZoneSoftAntiAffinity) if err != nil { - logrus.Errorf("Error getting replica zone soft anti-affinity setting: %v", err) + logrus.Errorf("Failed to get replica zone soft anti-affinity setting: %v", err) } if volume.Spec.ReplicaZoneSoftAntiAffinity != longhorn.ReplicaZoneSoftAntiAffinityDefault && volume.Spec.ReplicaZoneSoftAntiAffinity != "" { @@ -202,6 +202,11 @@ func (rcs *ReplicaScheduler) getDiskCandidates(nodeInfo map[string]*longhorn.Nod return result } + tolerateEmptyNodeSelectorVolume, err := rcs.ds.GetSettingAsBool(types.SettingNameTolerateEmptyNodeSelectorVolume) + if err != nil { + logrus.Errorf("Failed to get tolerate empty node selector volume setting: %v", err) + } + unusedNodes := map[string]*longhorn.Node{} unusedNodesInNewZones := map[string]*longhorn.Node{} nodesInUnusedZones := map[string]*longhorn.Node{} @@ -209,7 +214,7 @@ func (rcs *ReplicaScheduler) getDiskCandidates(nodeInfo map[string]*longhorn.Nod for nodeName, node := range nodeInfo { // Filter Nodes. If the Nodes don't match the tags, don't bother marking them as candidates. - if !types.IsSelectorsInTags(node.Spec.Tags, volume.Spec.NodeSelector) { + if !types.IsSelectorsInTags(node.Spec.Tags, volume.Spec.NodeSelector, tolerateEmptyNodeSelectorVolume) { continue } if _, ok := usedNodes[nodeName]; !ok { @@ -291,6 +296,11 @@ func (rcs *ReplicaScheduler) filterNodeDisksForReplica(node *longhorn.Node, disk multiError = util.NewMultiError() preferredDisks = map[string]*Disk{} + tolerateEmptyDiskSelectorVolume, err := rcs.ds.GetSettingAsBool(types.SettingNameTolerateEmptyDiskSelectorVolume) + if err != nil { + logrus.Errorf("Failed to get tolerate empty tag volume setting: %v", err) + } + if len(disks) == 0 { multiError.Append(util.NewMultiError(longhorn.ErrorReplicaScheduleDiskUnavailable)) return preferredDisks, multiError @@ -349,7 +359,7 @@ func (rcs *ReplicaScheduler) filterNodeDisksForReplica(node *longhorn.Node, disk } // Check if the Disk's Tags are valid. - if !types.IsSelectorsInTags(diskSpec.Tags, volume.Spec.DiskSelector) { + if !types.IsSelectorsInTags(diskSpec.Tags, volume.Spec.DiskSelector, tolerateEmptyDiskSelectorVolume) { multiError.Append(util.NewMultiError(longhorn.ErrorReplicaScheduleTagsNotFulfilled)) continue } @@ -559,6 +569,11 @@ func (rcs *ReplicaScheduler) isFailedReplicaReusable(r *longhorn.Replica, v *lon return false } + tolerateEmptyDiskSelectorVolume, err := rcs.ds.GetSettingAsBool(types.SettingNameTolerateEmptyDiskSelectorVolume) + if err != nil { + logrus.Errorf("Failed to get tolerate empty tag volume setting: %v", err) + } + node, exists := nodeInfo[r.Spec.NodeID] if !exists { return false @@ -595,7 +610,7 @@ func (rcs *ReplicaScheduler) isFailedReplicaReusable(r *longhorn.Replica, v *lon if !diskSpec.AllowScheduling || diskSpec.EvictionRequested { return false } - if !types.IsSelectorsInTags(diskSpec.Tags, v.Spec.DiskSelector) { + if !types.IsSelectorsInTags(diskSpec.Tags, v.Spec.DiskSelector, tolerateEmptyDiskSelectorVolume) { return false } } diff --git a/types/setting.go b/types/setting.go index 2ee748cea2..b3378d45ba 100644 --- a/types/setting.go +++ b/types/setting.go @@ -107,6 +107,8 @@ const ( SettingNameV2DataEngine = SettingName("v2-data-engine") SettingNameV2DataEngineHugepageLimit = SettingName("v2-data-engine-hugepage-limit") SettingNameOfflineReplicaRebuilding = SettingName("offline-replica-rebuilding") + SettingNameTolerateEmptyNodeSelectorVolume = SettingName("tolerate-empty-node-selector-volume") + SettingNameTolerateEmptyDiskSelectorVolume = SettingName("tolerate-empty-disk-selector-volume") ) var ( @@ -179,6 +181,8 @@ var ( SettingNameV2DataEngine, SettingNameV2DataEngineHugepageLimit, SettingNameOfflineReplicaRebuilding, + SettingNameTolerateEmptyNodeSelectorVolume, + SettingNameTolerateEmptyDiskSelectorVolume, } ) @@ -277,6 +281,8 @@ var ( SettingNameV2DataEngine: SettingDefinitionV2DataEngine, SettingNameV2DataEngineHugepageLimit: SettingDefinitionV2DataEngineHugepageLimit, SettingNameOfflineReplicaRebuilding: SettingDefinitionOfflineReplicaRebuilding, + SettingNameTolerateEmptyNodeSelectorVolume: SettingDefinitionTolerateEmptyNodeSelectorVolume, + SettingNameTolerateEmptyDiskSelectorVolume: SettingDefinitionTolerateEmptyDiskSelectorVolume, } SettingDefinitionBackupTarget = SettingDefinition{ @@ -1103,6 +1109,26 @@ var ( ReadOnly: true, Default: "1024", } + + SettingDefinitionTolerateEmptyNodeSelectorVolume = SettingDefinition{ + DisplayName: "Tolerate Empty Node Selector Volume", + Description: "Allow replica of the volume without node selector to be scheduled on node with tags, default true", + Category: SettingCategoryScheduling, + Type: SettingTypeBool, + Required: true, + ReadOnly: false, + Default: "true", + } + + SettingDefinitionTolerateEmptyDiskSelectorVolume = SettingDefinition{ + DisplayName: "Tolerate Empty Disk Selector Volume", + Description: "Allow replica of the volume without disk selector to be scheduled on disk with tags, default true", + Category: SettingCategoryScheduling, + Type: SettingTypeBool, + Required: true, + ReadOnly: false, + Default: "true", + } ) type NodeDownPodDeletionPolicy string @@ -1198,6 +1224,10 @@ func ValidateSetting(name, value string) (err error) { fallthrough case SettingNameV2DataEngine: fallthrough + case SettingNameTolerateEmptyNodeSelectorVolume: + fallthrough + case SettingNameTolerateEmptyDiskSelectorVolume: + fallthrough case SettingNameAllowCollectingLonghornUsage: if value != "true" && value != "false" { return fmt.Errorf("value %v of setting %v should be true or false", value, sName) diff --git a/types/types.go b/types/types.go index 953578af2c..44ef5264e3 100644 --- a/types/types.go +++ b/types/types.go @@ -995,12 +995,18 @@ func GetLHVolumeAttachmentNameFromVolumeName(volName string) string { // IsSelectorsInTags checks if all the selectors are present in the tags slice. // It returns true if all selectors are found, false otherwise. -func IsSelectorsInTags(tags, selectors []string) bool { +func IsSelectorsInTags(tags, selectors []string, tolerateEmptySelector bool) bool { if !sort.StringsAreSorted(tags) { logrus.Debug("BUG: Tags are not sorted, sorting now") sort.Strings(tags) } + if !tolerateEmptySelector { + if len(selectors) == 0 && len(tags) != 0 { + return false + } + } + for _, selector := range selectors { index := sort.SearchStrings(tags, selector) // If the selector is not found or the index is out of bounds, return false. diff --git a/types/types_test.go b/types/types_test.go index 020c9dd8ed..cf26feb31f 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -123,43 +123,55 @@ func (s *TestSuite) TestParseToleration(c *C) { func (s *TestSuite) TestIsSelectorsInTags(c *C) { type testCase struct { - inputTags []string - inputSelectors []string + inputTags []string + inputSelectors []string + tolerateEmptySelector bool expected bool } testCases := map[string]testCase{ "selectors exist": { - inputTags: []string{"aaa", "bbb", "ccc"}, - inputSelectors: []string{"aaa", "bbb", "ccc"}, - expected: true, + inputTags: []string{"aaa", "bbb", "ccc"}, + inputSelectors: []string{"aaa", "bbb", "ccc"}, + tolerateEmptySelector: true, + expected: true, }, "selectors mis-matched": { - inputTags: []string{"aaa", "bbb", "ccc"}, - inputSelectors: []string{"aaa", "b", "ccc"}, - expected: false, + inputTags: []string{"aaa", "bbb", "ccc"}, + inputSelectors: []string{"aaa", "b", "ccc"}, + tolerateEmptySelector: true, + expected: false, }, - "selectors empty": { - inputTags: []string{"aaa", "bbb", "ccc"}, - inputSelectors: []string{}, - expected: true, + "selectors empty and tolerate": { + inputTags: []string{"aaa", "bbb", "ccc"}, + inputSelectors: []string{}, + tolerateEmptySelector: true, + expected: true, + }, + "selectors empty and not tolerate": { + inputTags: []string{"aaa", "bbb", "ccc"}, + inputSelectors: []string{}, + tolerateEmptySelector: true, + expected: false, }, "tags unsorted": { - inputTags: []string{"bbb", "aaa", "ccc"}, - inputSelectors: []string{"aaa", "bbb", "ccc"}, - expected: true, + inputTags: []string{"bbb", "aaa", "ccc"}, + inputSelectors: []string{"aaa", "bbb", "ccc"}, + tolerateEmptySelector: true, + expected: true, }, "tags empty": { - inputTags: []string{}, - inputSelectors: []string{"aaa", "bbb", "ccc"}, - expected: false, + inputTags: []string{}, + inputSelectors: []string{"aaa", "bbb", "ccc"}, + tolerateEmptySelector: true, + expected: false, }, } for testName, testCase := range testCases { fmt.Printf("testing %v\n", testName) - actual := IsSelectorsInTags(testCase.inputTags, testCase.inputSelectors) + actual := IsSelectorsInTags(testCase.inputTags, testCase.inputSelectors, testCase.tolerateEmptySelector) c.Assert(actual, Equals, testCase.expected, Commentf(TestErrResultFmt, testName)) } }