mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-16 19:18:30 +02:00
admin: accept a list of collections in the task collection filter (#10953)
The collection filter was parsed twice with two syntaxes: the master-side volume listing compiled the whole string as one regex, while EC encode and EC balance detection split it on commas and matched each entry as a wildcard. A volume had to pass both, so "collection-a,collection-b" matched nothing (no collection is named that), and the ALL_COLLECTIONS sentinel, which the master side skips, dropped every volume at the task side. Parse it once, in one place: a comma-separated list where an entry is a name with optional * and ? wildcards, or a regex when it carries regex syntax. A regex entry now has to match the whole name unless it anchors itself, so listing a collection no longer picks up its longer namesakes.
This commit is contained in:
1 parent
ef4c9d9178
commit
e482e67971
17 files changed
+235
-48
No files matched your search
@@ -1,14 +1,89 @@
|
||||
package pluginworker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
||||
)
|
||||
|
||||
// CollectionFilterMode controls how collections are interpreted during
|
||||
// detection. The two recognized sentinels short-circuit regex matching:
|
||||
// detection. The two recognized sentinels short-circuit matching:
|
||||
// - CollectionFilterAll: pool every collection together (default).
|
||||
// - CollectionFilterEach: run detection separately per collection.
|
||||
//
|
||||
// Any other non-empty value is treated as a regex.
|
||||
// Any other non-empty value is a comma-separated list of collection patterns.
|
||||
type CollectionFilterMode string
|
||||
|
||||
const (
|
||||
CollectionFilterAll CollectionFilterMode = "ALL_COLLECTIONS"
|
||||
CollectionFilterEach CollectionFilterMode = "EACH_COLLECTION"
|
||||
)
|
||||
|
||||
// regexMetaCharacters mark a filter entry as a regex; "*" and "?" stay wildcards.
|
||||
const regexMetaCharacters = `.^$+()[]{}|\`
|
||||
|
||||
// CollectionMatcher matches a volume collection against a collection_filter value.
|
||||
type CollectionMatcher struct {
|
||||
wildcards []string
|
||||
regexes []*regexp.Regexp
|
||||
}
|
||||
|
||||
// CompileCollectionMatcher parses a collection_filter into a matcher. A nil
|
||||
// matcher accepts every collection: that is the empty filter, "*", and both
|
||||
// mode sentinels. Any other value is a comma-separated list, and a collection
|
||||
// passes when one entry matches it. An entry is a name, optionally with "*" and
|
||||
// "?" wildcards, or a regex when it carries regex syntax. A regex entry must
|
||||
// match the whole name unless it anchors itself with "^" or "$".
|
||||
func CompileCollectionMatcher(filter string) (*CollectionMatcher, error) {
|
||||
trimmed := strings.TrimSpace(filter)
|
||||
mode := CollectionFilterMode(trimmed)
|
||||
if trimmed == "" || trimmed == "*" || mode == CollectionFilterAll || mode == CollectionFilterEach {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
matcher := &CollectionMatcher{}
|
||||
for _, entry := range strings.Split(trimmed, ",") {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.ContainsAny(entry, regexMetaCharacters) {
|
||||
matcher.wildcards = append(matcher.wildcards, entry)
|
||||
continue
|
||||
}
|
||||
pattern := entry
|
||||
if !strings.ContainsAny(entry, "^$") {
|
||||
pattern = "^(?:" + entry + ")$"
|
||||
}
|
||||
compiled, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid collection_filter entry %q: %w", entry, err)
|
||||
}
|
||||
matcher.regexes = append(matcher.regexes, compiled)
|
||||
}
|
||||
|
||||
if len(matcher.wildcards) == 0 && len(matcher.regexes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return matcher, nil
|
||||
}
|
||||
|
||||
// Matches reports whether a collection passes the filter. A nil matcher accepts everything.
|
||||
func (m *CollectionMatcher) Matches(collection string) bool {
|
||||
if m == nil {
|
||||
return true
|
||||
}
|
||||
for _, pattern := range m.wildcards {
|
||||
if wildcard.MatchesWildcard(pattern, collection) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, re := range m.regexes {
|
||||
if re.MatchString(collection) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package pluginworker
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCompileCollectionMatcher(t *testing.T) {
|
||||
cases := []struct {
|
||||
filter string
|
||||
matches []string
|
||||
misses []string
|
||||
}{
|
||||
{filter: "", matches: []string{"", "photos"}},
|
||||
{filter: "*", matches: []string{"", "photos"}},
|
||||
{filter: string(CollectionFilterAll), matches: []string{"", "photos"}},
|
||||
{filter: string(CollectionFilterEach), matches: []string{"", "photos"}},
|
||||
{filter: "photos", matches: []string{"photos"}, misses: []string{"", "photos-backup", "myphotos"}},
|
||||
{filter: "collection-a,collection-b", matches: []string{"collection-a", "collection-b"}, misses: []string{"collection-c"}},
|
||||
{filter: " collection-a , collection-b ,", matches: []string{"collection-a", "collection-b"}, misses: []string{"collection-c"}},
|
||||
{filter: "photos*,videos", matches: []string{"photos", "photos-backup", "videos"}, misses: []string{"clips"}},
|
||||
{filter: "photo?", matches: []string{"photos"}, misses: []string{"photo", "photos-backup"}},
|
||||
// A regex entry matches the whole name unless it anchors itself.
|
||||
{filter: "photos|videos", matches: []string{"photos", "videos"}, misses: []string{"photos-backup"}},
|
||||
{filter: "^photos", matches: []string{"photos", "photos-backup"}, misses: []string{"videos"}},
|
||||
{filter: "photos-.*,videos", matches: []string{"photos-backup", "videos"}, misses: []string{"photos"}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
matcher, err := CompileCollectionMatcher(c.filter)
|
||||
if err != nil {
|
||||
t.Fatalf("CompileCollectionMatcher(%q): %v", c.filter, err)
|
||||
}
|
||||
for _, collection := range c.matches {
|
||||
if !matcher.Matches(collection) {
|
||||
t.Errorf("filter %q should match collection %q", c.filter, collection)
|
||||
}
|
||||
}
|
||||
for _, collection := range c.misses {
|
||||
if matcher.Matches(collection) {
|
||||
t.Errorf("filter %q should not match collection %q", c.filter, collection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileCollectionMatcherInvalidEntry(t *testing.T) {
|
||||
if _, err := CompileCollectionMatcher("photos,[invalid"); err == nil {
|
||||
t.Fatal("expected an error for an unparsable entry")
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -137,15 +136,9 @@ func buildVolumeMetrics(
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
var collectionRegex *regexp.Regexp
|
||||
trimmedFilter := strings.TrimSpace(collectionFilter)
|
||||
filterMode := CollectionFilterMode(trimmedFilter)
|
||||
if trimmedFilter != "" && filterMode != CollectionFilterAll && filterMode != CollectionFilterEach && trimmedFilter != "*" {
|
||||
var err error
|
||||
collectionRegex, err = regexp.Compile(trimmedFilter)
|
||||
if err != nil {
|
||||
return nil, nil, nil, &configError{err: fmt.Errorf("invalid collection_filter regex %q: %w", trimmedFilter, err)}
|
||||
}
|
||||
collectionMatcher, err := CompileCollectionMatcher(collectionFilter)
|
||||
if err != nil {
|
||||
return nil, nil, nil, &configError{err: err}
|
||||
}
|
||||
|
||||
volumeSizeLimitBytes := uint64(response.VolumeSizeLimitMb) * 1024 * 1024
|
||||
@@ -167,7 +160,7 @@ func buildVolumeMetrics(
|
||||
Host: pb.NewServerAddressFromDataNode(node).ToHost(),
|
||||
})
|
||||
|
||||
if collectionRegex != nil && !collectionRegex.MatchString(volume.Collection) {
|
||||
if !collectionMatcher.Matches(volume.Collection) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,26 @@ func TestBuildVolumeMetricsRegexFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildVolumeMetricsCollectionList(t *testing.T) {
|
||||
resp := makeTestVolumeListResponse(
|
||||
&master_pb.VolumeInformationMessage{Id: 1, Collection: "photos", Size: 100},
|
||||
&master_pb.VolumeInformationMessage{Id: 2, Collection: "videos", Size: 200},
|
||||
&master_pb.VolumeInformationMessage{Id: 3, Collection: "clips", Size: 300},
|
||||
)
|
||||
metrics, _, _, err := buildVolumeMetrics(resp, "photos,videos")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(metrics) != 2 {
|
||||
t.Fatalf("expected 2 metrics, got %d", len(metrics))
|
||||
}
|
||||
for _, metric := range metrics {
|
||||
if metric.Collection != "photos" && metric.Collection != "videos" {
|
||||
t.Fatalf("unexpected collection %q", metric.Collection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildVolumeMetricsInvalidRegex(t *testing.T) {
|
||||
resp := makeTestVolumeListResponse(
|
||||
&master_pb.VolumeInformationMessage{Id: 1, Collection: "photos", Size: 100},
|
||||
|
||||
Reference in new issue
Block a user