diff --git a/staging/src/k8s.io/apimachinery/pkg/labels/labels.go b/staging/src/k8s.io/apimachinery/pkg/labels/labels.go index 8360d842b6c..19d823cef7b 100644 --- a/staging/src/k8s.io/apimachinery/pkg/labels/labels.go +++ b/staging/src/k8s.io/apimachinery/pkg/labels/labels.go @@ -77,6 +77,8 @@ func (ls Set) AsValidatedSelector() (Selector, error) { // perform any validation. // According to our measurements this is significantly faster // in codepaths that matter at high scale. +// Note: this method copies the Set; if the Set is immutable, consider wrapping it with ValidatedSetSelector +// instead, which does not copy. func (ls Set) AsSelectorPreValidated() Selector { return SelectorFromValidatedSet(ls) } diff --git a/staging/src/k8s.io/apimachinery/pkg/labels/selector.go b/staging/src/k8s.io/apimachinery/pkg/labels/selector.go index 8910043890c..a64c66aca08 100644 --- a/staging/src/k8s.io/apimachinery/pkg/labels/selector.go +++ b/staging/src/k8s.io/apimachinery/pkg/labels/selector.go @@ -948,6 +948,8 @@ func ValidatedSelectorFromSet(ls Set) (Selector, error) { // SelectorFromValidatedSet returns a Selector which will match exactly the given Set. // A nil and empty Sets are considered equivalent to Everything(). // It assumes that Set is already validated and doesn't do any validation. +// Note: this method copies the Set; if the Set is immutable, consider wrapping it with ValidatedSetSelector +// instead, which does not copy. func SelectorFromValidatedSet(ls Set) Selector { if ls == nil || len(ls) == 0 { return internalSelector{} @@ -969,3 +971,76 @@ func SelectorFromValidatedSet(ls Set) Selector { func ParseToRequirements(selector string, opts ...field.PathOption) ([]Requirement, error) { return parse(selector, field.ToPath(opts...)) } + +// ValidatedSetSelector wraps a Set, allowing it to implement the Selector interface. Unlike +// Set.AsSelectorPreValidated (which copies the input Set), this type simply wraps the underlying +// Set. As a result, it is substantially more efficient. A nil and empty Sets are considered +// equivalent to Everything(). +// +// Callers MUST ensure the underlying Set is not mutated, and that it is already validated. If these +// constraints are not met, Set.AsValidatedSelector should be preferred +// +// None of the Selector methods mutate the underlying Set, but Add() and Requirements() convert to +// the less optimized version. +type ValidatedSetSelector Set + +func (s ValidatedSetSelector) Matches(labels Labels) bool { + for k, v := range s { + if !labels.Has(k) || v != labels.Get(k) { + return false + } + } + return true +} + +func (s ValidatedSetSelector) Empty() bool { + return len(s) == 0 +} + +func (s ValidatedSetSelector) String() string { + keys := make([]string, 0, len(s)) + for k := range s { + keys = append(keys, k) + } + // Ensure deterministic output + sort.Strings(keys) + b := strings.Builder{} + for i, key := range keys { + v := s[key] + b.Grow(len(key) + 2 + len(v)) + if i != 0 { + b.WriteString(",") + } + b.WriteString(key) + b.WriteString("=") + b.WriteString(v) + } + return b.String() +} + +func (s ValidatedSetSelector) Add(r ...Requirement) Selector { + return s.toFullSelector().Add(r...) +} + +func (s ValidatedSetSelector) Requirements() (requirements Requirements, selectable bool) { + return s.toFullSelector().Requirements() +} + +func (s ValidatedSetSelector) DeepCopySelector() Selector { + res := make(ValidatedSetSelector, len(s)) + for k, v := range s { + res[k] = v + } + return res +} + +func (s ValidatedSetSelector) RequiresExactMatch(label string) (value string, found bool) { + v, f := s[label] + return v, f +} + +func (s ValidatedSetSelector) toFullSelector() Selector { + return SelectorFromValidatedSet(Set(s)) +} + +var _ Selector = ValidatedSetSelector{} diff --git a/staging/src/k8s.io/apimachinery/pkg/labels/selector_test.go b/staging/src/k8s.io/apimachinery/pkg/labels/selector_test.go index 9d2730284c0..4471969297e 100644 --- a/staging/src/k8s.io/apimachinery/pkg/labels/selector_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/labels/selector_test.go @@ -809,11 +809,70 @@ func BenchmarkSelectorFromValidatedSet(b *testing.B) { "foo": "foo", "bar": "bar", } + matchee := Set(map[string]string{ + "foo": "foo", + "bar": "bar", + "extra": "label", + }) for i := 0; i < b.N; i++ { - if SelectorFromValidatedSet(set).Empty() { + s := SelectorFromValidatedSet(set) + if s.Empty() { b.Errorf("Unexpected selector") } + if !s.Matches(matchee) { + b.Errorf("Unexpected match") + } + } +} + +func BenchmarkSetSelector(b *testing.B) { + set := map[string]string{ + "foo": "foo", + "bar": "bar", + } + matchee := Set(map[string]string{ + "foo": "foo", + "bar": "bar", + "extra": "label", + }) + + for i := 0; i < b.N; i++ { + s := ValidatedSetSelector(set) + if s.Empty() { + b.Errorf("Unexpected selector") + } + if !s.Matches(matchee) { + b.Errorf("Unexpected match") + } + } +} + +func TestSetSelectorString(t *testing.T) { + cases := []struct { + set Set + out string + }{ + { + Set{}, + "", + }, + { + Set{"app": "foo"}, + "app=foo", + }, + { + Set{"app": "foo", "a": "b"}, + "a=b,app=foo", + }, + } + + for _, tt := range cases { + t.Run(tt.out, func(t *testing.T) { + if got := ValidatedSetSelector(tt.set).String(); tt.out != got { + t.Fatalf("expected %v, got %v", tt.out, got) + } + }) } }