istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/label/filter.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package label
    16  
    17  import (
    18  	"fmt"
    19  	"regexp"
    20  	"strings"
    21  
    22  	"istio.io/istio/pkg/log"
    23  )
    24  
    25  // Selector is a Set of label filter expressions that get applied together to decide whether tests should be selected
    26  // for execution or not.
    27  type Selector struct {
    28  	// The constraints are and'ed together.
    29  	present Set
    30  	absent  Set
    31  }
    32  
    33  var _ fmt.Stringer = Selector{}
    34  
    35  // NewSelector returns a new selector based on the given presence/absence predicates.
    36  func NewSelector(present []Instance, absent []Instance) Selector {
    37  	return Selector{
    38  		present: NewSet(present...),
    39  		absent:  NewSet(absent...),
    40  	}
    41  }
    42  
    43  var userLabelRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z0-9_]+)*$`)
    44  
    45  // ParseSelector parses and returns a new instance of Selector.
    46  func ParseSelector(s string) (Selector, error) {
    47  	var present, absent []Instance
    48  
    49  	parts := strings.Split(s, ",")
    50  	for _, p := range parts {
    51  		if len(p) == 0 {
    52  			continue
    53  		}
    54  
    55  		var negative bool
    56  		switch p[0] {
    57  		case '-':
    58  			negative = true
    59  			p = p[1:]
    60  		case '+':
    61  			p = p[1:]
    62  		}
    63  
    64  		if !userLabelRegex.MatchString(p) {
    65  			return Selector{}, fmt.Errorf("invalid label name: %q", p)
    66  		}
    67  
    68  		l := Instance(p)
    69  		if !all.contains(l) {
    70  			log.Warnf("unknown label name: %q", p)
    71  			continue
    72  		}
    73  
    74  		if negative {
    75  			absent = append(absent, l)
    76  		} else {
    77  			present = append(present, l)
    78  		}
    79  	}
    80  
    81  	pSet := NewSet(present...)
    82  	aSet := NewSet(absent...)
    83  
    84  	if pSet.containsAny(aSet) || aSet.containsAny(pSet) {
    85  		return Selector{}, fmt.Errorf("conflicting selector specification: %q", s)
    86  	}
    87  
    88  	return NewSelector(present, absent), nil
    89  }
    90  
    91  // Selects returns true, if the given label set satisfies the Selector.
    92  func (f *Selector) Selects(inputs Set) bool {
    93  	return !inputs.containsAny(f.absent) && inputs.containsAll(f.present)
    94  }
    95  
    96  // Excludes returns false, if the given set of labels, even combined with new ones, could end up satisfying the Selector.
    97  // It returns false, if Matches would never return true, even if new labels are added to the input set.
    98  func (f *Selector) Excludes(inputs Set) bool {
    99  	return inputs.containsAny(f.absent)
   100  }
   101  
   102  func (f Selector) String() string {
   103  	var result string
   104  
   105  	for _, p := range f.present.All() {
   106  		if result != "" {
   107  			result += ","
   108  		}
   109  		result += "+" + string(p)
   110  	}
   111  
   112  	for _, p := range f.absent.All() {
   113  		if result != "" {
   114  			result += ","
   115  		}
   116  		result += "-" + string(p)
   117  	}
   118  
   119  	return result
   120  }