mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 15:05:27 +00:00
Extending label.Parse method to support exact match
This commit is contained in:
parent
86434b4038
commit
0186acc37c
@ -19,7 +19,6 @@ package labels
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@ -38,7 +37,6 @@ type Selector interface {
|
||||
// RequiresExactMatch allows a caller to introspect whether a given selector
|
||||
// requires a single specific label to be set, and if so returns the value it
|
||||
// requires.
|
||||
// TODO: expand this to be more general
|
||||
RequiresExactMatch(label string) (value string, found bool)
|
||||
|
||||
// String returns a human readable string that represents this selector.
|
||||
@ -139,43 +137,40 @@ func (t andTerm) String() string {
|
||||
return strings.Join(terms, ",")
|
||||
}
|
||||
|
||||
// TODO Support forward and reverse indexing (#1183, #1348). Eliminate uses of Selector.RequiresExactMatch.
|
||||
// TODO rename to Selector after Selector interface above removed
|
||||
type SetBasedSelector interface {
|
||||
// Matches returns true if this selector matches the given set of labels.
|
||||
Matches(Labels) (bool, error)
|
||||
|
||||
// String returns a human-readable string that represents this selector.
|
||||
String() (string, error)
|
||||
}
|
||||
|
||||
// Operator represents a key's relationship
|
||||
// to a set of values in a Requirement.
|
||||
type Operator int
|
||||
type Operator string
|
||||
|
||||
const (
|
||||
In Operator = iota + 1
|
||||
NotIn
|
||||
Exists
|
||||
EqualsOperator Operator = "="
|
||||
DoubleEqualsOperator Operator = "=="
|
||||
InOperator Operator = "in"
|
||||
NotEqualsOperator Operator = "!="
|
||||
NotInOperator Operator = "notin"
|
||||
ExistsOperator Operator = "exists"
|
||||
)
|
||||
|
||||
// LabelSelector contains a list of Requirements.
|
||||
// LabelSelector is set-based and is distinguished from exact
|
||||
// match-based selectors composed of key=value matching conjunctions.
|
||||
// TODO: Remove previous sentence when exact match-based
|
||||
// selectors are removed.
|
||||
type LabelSelector struct {
|
||||
Requirements []Requirement
|
||||
}
|
||||
|
||||
// Sort by obtain determisitic parser (minimic previous andTerm based stuff)
|
||||
type ByKey []Requirement
|
||||
|
||||
func (a ByKey) Len() int { return len(a) }
|
||||
|
||||
func (a ByKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
||||
func (a ByKey) Less(i, j int) bool { return a[i].key < a[j].key }
|
||||
|
||||
// Requirement is a selector that contains values, a key
|
||||
// and an operator that relates the key and values. The zero
|
||||
// value of Requirement is invalid. See the NewRequirement
|
||||
// constructor for creating a valid Requirement.
|
||||
// Requirement is set-based and is distinguished from exact
|
||||
// match-based selectors composed of key=value matching.
|
||||
// TODO: Remove previous sentence when exact match-based
|
||||
// selectors are removed.
|
||||
// value of Requirement is invalid.
|
||||
// Requirement implements both set based match and exact match
|
||||
// Requirement is initialized via NewRequirement constructor for creating a valid Requirement.
|
||||
type Requirement struct {
|
||||
key string
|
||||
operator Operator
|
||||
@ -196,14 +191,24 @@ func NewRequirement(key string, op Operator, vals util.StringSet) (*Requirement,
|
||||
return nil, err
|
||||
}
|
||||
switch op {
|
||||
case In, NotIn:
|
||||
case InOperator, NotInOperator:
|
||||
if len(vals) == 0 {
|
||||
return nil, fmt.Errorf("for In,NotIn operators, values set can't be empty")
|
||||
}
|
||||
case Exists:
|
||||
case EqualsOperator, DoubleEqualsOperator, NotEqualsOperator:
|
||||
if len(vals) != 1 {
|
||||
return nil, fmt.Errorf("exact match compatibility requires one single value")
|
||||
}
|
||||
case ExistsOperator:
|
||||
default:
|
||||
return nil, fmt.Errorf("operator '%v' is not recognized", op)
|
||||
}
|
||||
|
||||
for v := range vals {
|
||||
if err := validateLabelValue(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &Requirement{key: key, operator: op, strValues: vals}, nil
|
||||
}
|
||||
|
||||
@ -216,86 +221,484 @@ func NewRequirement(key string, op Operator, vals util.StringSet) (*Requirement,
|
||||
// Labels' value for that key is not in Requirement's value set.
|
||||
// (4) The operator is NotIn and Labels does not have the
|
||||
// Requirement's key.
|
||||
//
|
||||
// If called on an invalid Requirement, an error is returned. See
|
||||
// NewRequirement for creating a valid Requirement.
|
||||
func (r *Requirement) Matches(ls Labels) (bool, error) {
|
||||
func (r *Requirement) Matches(ls Labels) bool {
|
||||
switch r.operator {
|
||||
case In:
|
||||
case InOperator, EqualsOperator, DoubleEqualsOperator:
|
||||
if !ls.Has(r.key) {
|
||||
return false, nil
|
||||
return false
|
||||
}
|
||||
return r.strValues.Has(ls.Get(r.key)), nil
|
||||
case NotIn:
|
||||
return r.strValues.Has(ls.Get(r.key))
|
||||
case NotInOperator, NotEqualsOperator:
|
||||
if !ls.Has(r.key) {
|
||||
return true, nil
|
||||
return true
|
||||
}
|
||||
return !r.strValues.Has(ls.Get(r.key)), nil
|
||||
case Exists:
|
||||
return ls.Has(r.key), nil
|
||||
return !r.strValues.Has(ls.Get(r.key))
|
||||
case ExistsOperator:
|
||||
return ls.Has(r.key)
|
||||
default:
|
||||
return false, fmt.Errorf("requirement is not set: %+v", r)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if the LabelSelector doesn't restrict selection space
|
||||
func (lsel LabelSelector) Empty() bool {
|
||||
if len(lsel.Requirements) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// RequiresExactMatch allows a caller to introspect whether a given selector
|
||||
// requires a single specific label to be set, and if so returns the value it
|
||||
// requires.
|
||||
func (r *Requirement) RequiresExactMatch(label string) (string, bool) {
|
||||
if len(r.strValues) == 1 && r.operator == InOperator && r.key == label {
|
||||
return r.strValues.List()[0], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// String returns a human-readable string that represents this
|
||||
// Requirement. If called on an invalid Requirement, an error is
|
||||
// returned. See NewRequirement for creating a valid Requirement.
|
||||
func (r *Requirement) String() (string, error) {
|
||||
func (r *Requirement) String() string {
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(r.key)
|
||||
|
||||
switch r.operator {
|
||||
case In:
|
||||
case EqualsOperator:
|
||||
buffer.WriteString("=")
|
||||
case DoubleEqualsOperator:
|
||||
buffer.WriteString("==")
|
||||
case NotEqualsOperator:
|
||||
buffer.WriteString("!=")
|
||||
case InOperator:
|
||||
buffer.WriteString(" in ")
|
||||
case NotIn:
|
||||
buffer.WriteString(" not in ")
|
||||
case Exists:
|
||||
return buffer.String(), nil
|
||||
default:
|
||||
return "", fmt.Errorf("requirement is not set: %+v", r)
|
||||
case NotInOperator:
|
||||
buffer.WriteString(" notin ")
|
||||
case ExistsOperator:
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
buffer.WriteString("(")
|
||||
switch r.operator {
|
||||
case InOperator, NotInOperator:
|
||||
buffer.WriteString("(")
|
||||
}
|
||||
if len(r.strValues) == 1 {
|
||||
buffer.WriteString(r.strValues.List()[0])
|
||||
} else { // only > 1 since == 0 prohibited by NewRequirement
|
||||
buffer.WriteString(strings.Join(r.strValues.List(), ","))
|
||||
}
|
||||
buffer.WriteString(")")
|
||||
return buffer.String(), nil
|
||||
|
||||
switch r.operator {
|
||||
case InOperator, NotInOperator:
|
||||
buffer.WriteString(")")
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// Matches for a LabelSelector returns true if all
|
||||
// its Requirements match the input Labels. If any
|
||||
// Requirement does not match, false is returned.
|
||||
// An error is returned if any match attempt between
|
||||
// a Requirement and the input Labels returns an error.
|
||||
func (lsel *LabelSelector) Matches(l Labels) (bool, error) {
|
||||
func (lsel LabelSelector) Matches(l Labels) bool {
|
||||
for _, req := range lsel.Requirements {
|
||||
if matches, err := req.Matches(l); err != nil {
|
||||
return false, err
|
||||
} else if !matches {
|
||||
return false, nil
|
||||
if matches := req.Matches(l); !matches {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
return true
|
||||
}
|
||||
|
||||
// RequiresExactMatch allows a caller to introspect whether a given selector
|
||||
// requires a single specific label to be set, and if so returns the value it
|
||||
// requires.
|
||||
func (lsel LabelSelector) RequiresExactMatch(label string) (value string, found bool) {
|
||||
for _, req := range lsel.Requirements {
|
||||
if value, found = req.RequiresExactMatch(label); found {
|
||||
return value, found
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// String returns a comma-separated string of all
|
||||
// the LabelSelector Requirements' human-readable strings.
|
||||
// An error is returned if any attempt to get a
|
||||
// Requirement's human-readable string returns an error.
|
||||
func (lsel *LabelSelector) String() (string, error) {
|
||||
func (lsel LabelSelector) String() string {
|
||||
var reqs []string
|
||||
for _, req := range lsel.Requirements {
|
||||
if str, err := req.String(); err != nil {
|
||||
return "", err
|
||||
reqs = append(reqs, req.String())
|
||||
}
|
||||
return strings.Join(reqs, ",")
|
||||
}
|
||||
|
||||
// constants definition for lexer token
|
||||
type Token int
|
||||
|
||||
const (
|
||||
ERROR Token = iota
|
||||
EOS // end of string
|
||||
CPAR
|
||||
COMMA
|
||||
EEQUAL
|
||||
EQUAL
|
||||
IDENTIFIER
|
||||
IN
|
||||
NEQUAL
|
||||
NOTIN
|
||||
OPAR
|
||||
OR
|
||||
)
|
||||
|
||||
// string2token contains the mapping between lexer Token and token literal
|
||||
// (except IDENTIFIER, EOS and ERROR since it makes no sense)
|
||||
var string2token = map[string]Token{
|
||||
")": CPAR,
|
||||
",": COMMA,
|
||||
"==": EEQUAL,
|
||||
"=": EQUAL,
|
||||
"in": IN,
|
||||
"!=": NEQUAL,
|
||||
"notin": NOTIN,
|
||||
"(": OPAR,
|
||||
}
|
||||
|
||||
// The item produced by the lexer. It contains the Token and the literal.
|
||||
type ScannedItem struct {
|
||||
tok Token
|
||||
literal string
|
||||
}
|
||||
|
||||
// isWhitespace returns true if the rune is a space, tab, or newline.
|
||||
func isWhitespace(ch byte) bool {
|
||||
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'
|
||||
}
|
||||
|
||||
// isSpecialSymbol detect if the character ch can be an operator
|
||||
func isSpecialSymbol(ch byte) bool {
|
||||
switch ch {
|
||||
case '=', '!', '(', ')', ',':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Lexer struct
|
||||
type Lexer struct {
|
||||
// s stores the string to be lexed
|
||||
s string
|
||||
// pos is the position currently lexed
|
||||
pos int
|
||||
}
|
||||
|
||||
// read return the character currently lexed
|
||||
// increment the position and check the buffer overflow
|
||||
func (l *Lexer) read() (b byte) {
|
||||
b = 0
|
||||
if l.pos < len(l.s) {
|
||||
b = l.s[l.pos]
|
||||
l.pos++
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// no return simply unread
|
||||
func (l *Lexer) unread() {
|
||||
l.pos--
|
||||
}
|
||||
|
||||
// func return a literal token (for example IN) and or an identifier.
|
||||
func (l *Lexer) scanIdOrKeyword() (tok Token, lit string) {
|
||||
var buffer []byte
|
||||
for {
|
||||
if ch := l.read(); ch == 0 { // end of string found
|
||||
break
|
||||
} else if isSpecialSymbol(ch) || isWhitespace(ch) {
|
||||
l.unread() // stop scanning and unread
|
||||
break
|
||||
} else {
|
||||
reqs = append(reqs, str)
|
||||
buffer = append(buffer, ch)
|
||||
}
|
||||
}
|
||||
return strings.Join(reqs, ","), nil
|
||||
s := string(buffer)
|
||||
if val, ok := string2token[s]; ok { // is a literal token?
|
||||
return val, s
|
||||
}
|
||||
return IDENTIFIER, s // otherwise is an identifier
|
||||
}
|
||||
|
||||
// scan string starting with specail symbol. At the moment this special symbols
|
||||
// identify not literal operators
|
||||
func (l *Lexer) scanSpecialSymbol() (Token, string) {
|
||||
lastScannedItem := ScannedItem{}
|
||||
var buffer []byte
|
||||
for {
|
||||
if ch := l.read(); ch == 0 {
|
||||
break
|
||||
} else if isSpecialSymbol(ch) {
|
||||
buffer = append(buffer, ch)
|
||||
if token, ok := string2token[string(buffer)]; ok {
|
||||
lastScannedItem = ScannedItem{tok: token, literal: string(buffer)}
|
||||
} else if lastScannedItem.tok != 0 {
|
||||
l.unread()
|
||||
break
|
||||
}
|
||||
} else { // in any other cases (identifer or whitespace) stop
|
||||
l.unread()
|
||||
break
|
||||
}
|
||||
}
|
||||
if lastScannedItem.tok == 0 {
|
||||
return ERROR, fmt.Sprintf("error expected keyword found '%s'", buffer)
|
||||
} else {
|
||||
return lastScannedItem.tok, lastScannedItem.literal
|
||||
}
|
||||
}
|
||||
|
||||
// func Lex return Token and the literal (meaningfull only in case of IDENTIFIER)
|
||||
func (l *Lexer) Lex() (tok Token, lit string) {
|
||||
ch := l.read()
|
||||
for { // consume spaces until no more spaces
|
||||
if !isWhitespace(ch) {
|
||||
break
|
||||
}
|
||||
ch = l.read()
|
||||
}
|
||||
if ch == 0 { // end of the string?
|
||||
return EOS, ""
|
||||
} else if isSpecialSymbol(ch) {
|
||||
l.unread()
|
||||
return l.scanSpecialSymbol() // can be an operator
|
||||
} else {
|
||||
l.unread()
|
||||
return l.scanIdOrKeyword()
|
||||
}
|
||||
}
|
||||
|
||||
// Parser data structure contains the label selector parser data and algos
|
||||
type Parser struct {
|
||||
l *Lexer
|
||||
scannedItems []ScannedItem
|
||||
position int
|
||||
}
|
||||
|
||||
type ParserContext int
|
||||
|
||||
const (
|
||||
KeyAndOperator ParserContext = iota
|
||||
Values
|
||||
)
|
||||
|
||||
// lookahead func returns the current token and string. No increment of current position
|
||||
func (p *Parser) lookahead(context ParserContext) (Token, string) {
|
||||
tok, lit := p.scannedItems[p.position].tok, p.scannedItems[p.position].literal
|
||||
if context == Values {
|
||||
switch tok {
|
||||
case IN, NOTIN:
|
||||
tok = IDENTIFIER
|
||||
}
|
||||
}
|
||||
return tok, lit
|
||||
}
|
||||
|
||||
// return current token and string. Increments the the position
|
||||
func (p *Parser) consume(context ParserContext) (Token, string) {
|
||||
p.position++
|
||||
tok, lit := p.scannedItems[p.position-1].tok, p.scannedItems[p.position-1].literal
|
||||
if context == Values {
|
||||
switch tok {
|
||||
case IN, NOTIN:
|
||||
tok = IDENTIFIER
|
||||
}
|
||||
}
|
||||
return tok, lit
|
||||
}
|
||||
|
||||
// scan method scan all the input string and storin <token, literal> pairs in
|
||||
// scanned items slice.
|
||||
// The Parser can now lookahead and consume the tokens
|
||||
func (p *Parser) scan() {
|
||||
for {
|
||||
token, literal := p.l.Lex()
|
||||
p.scannedItems = append(p.scannedItems, ScannedItem{token, literal})
|
||||
if token == EOS {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the entry function to parse list of requirements
|
||||
func (p *Parser) parse() ([]Requirement, error) {
|
||||
p.scan() // init scannedItems
|
||||
|
||||
var requirements []Requirement
|
||||
for {
|
||||
tok, lit := p.lookahead(Values)
|
||||
switch tok {
|
||||
case IDENTIFIER:
|
||||
r, err := p.parseRequirement()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error: ", err)
|
||||
}
|
||||
requirements = append(requirements, *r)
|
||||
t, l := p.consume(Values)
|
||||
switch t {
|
||||
case EOS:
|
||||
return requirements, nil
|
||||
case COMMA:
|
||||
t2, l2 := p.lookahead(Values)
|
||||
if t2 != IDENTIFIER {
|
||||
return nil, fmt.Errorf("Expected identifier after comma, found '%s'", l2)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Bad value '%s', expetected comma or 'end of string'", l)
|
||||
}
|
||||
case EOS:
|
||||
return requirements, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Bad value %s. Expected identifier or 'end of string'", lit)
|
||||
}
|
||||
}
|
||||
return requirements, nil
|
||||
}
|
||||
|
||||
// parse a Requirement data structure
|
||||
func (p *Parser) parseRequirement() (*Requirement, error) {
|
||||
key, operator, err := p.parseKeyAndInferOperator()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if operator == ExistsOperator { // operator Exists found lookahead set checked
|
||||
return NewRequirement(key, operator, nil)
|
||||
}
|
||||
operator, err = p.parseOperator()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var values util.StringSet
|
||||
switch operator {
|
||||
case InOperator, NotInOperator:
|
||||
values, err = p.parseValues()
|
||||
case EqualsOperator, DoubleEqualsOperator, NotEqualsOperator:
|
||||
values, err = p.parseExactValue()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewRequirement(key, operator, values)
|
||||
|
||||
}
|
||||
|
||||
// parseKeyAndInferOperator parse literals.
|
||||
func (p *Parser) parseKeyAndInferOperator() (string, Operator, error) {
|
||||
tok, literal := p.consume(Values)
|
||||
if tok != IDENTIFIER {
|
||||
err := fmt.Errorf("Found '%s' instead of expected IDENTIFIER", literal)
|
||||
return "", "", err
|
||||
}
|
||||
if err := validateLabelKey(literal); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
var operator Operator
|
||||
if t, _ := p.lookahead(Values); t == EOS || t == COMMA {
|
||||
operator = ExistsOperator
|
||||
}
|
||||
return literal, operator, nil
|
||||
}
|
||||
|
||||
// parseOperator return operator and eventually matchType
|
||||
// matchType can be exact
|
||||
func (p *Parser) parseOperator() (op Operator, err error) {
|
||||
tok, lit := p.consume(KeyAndOperator)
|
||||
switch tok {
|
||||
case IN:
|
||||
op = InOperator
|
||||
case EQUAL:
|
||||
op = EqualsOperator
|
||||
case EEQUAL:
|
||||
op = DoubleEqualsOperator
|
||||
case NOTIN:
|
||||
op = NotInOperator
|
||||
case NEQUAL:
|
||||
op = NotEqualsOperator
|
||||
default:
|
||||
return "", fmt.Errorf("Expected '=', '!=', '==', 'in', notin', found %s", lit)
|
||||
}
|
||||
return op, nil
|
||||
}
|
||||
|
||||
// parse values parse the values for set based matching (x,y,z)
|
||||
func (p *Parser) parseValues() (util.StringSet, error) {
|
||||
tok, lit := p.consume(Values)
|
||||
if tok != OPAR {
|
||||
return nil, fmt.Errorf("Found '%s' expected '('", lit)
|
||||
}
|
||||
tok, lit = p.lookahead(Values)
|
||||
switch tok {
|
||||
case IDENTIFIER, COMMA:
|
||||
s, err := p.parseIdentifiersList() // handles general cases
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
if tok, _ = p.consume(Values); tok != CPAR {
|
||||
return nil, fmt.Errorf("Expected a ')', found '%s'", lit)
|
||||
}
|
||||
return s, nil
|
||||
case CPAR: // handles "()"
|
||||
p.consume(Values)
|
||||
return util.NewStringSet(""), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Expected ')' or ',' or identifier. Found '%s'", lit)
|
||||
}
|
||||
return util.NewStringSet(), nil
|
||||
}
|
||||
|
||||
// parseIdentifiersList parse a (possibly empty) list of
|
||||
// of comma separated (possibly empty) identifiers
|
||||
func (p *Parser) parseIdentifiersList() (util.StringSet, error) {
|
||||
s := util.NewStringSet()
|
||||
for {
|
||||
tok, lit := p.consume(Values)
|
||||
switch tok {
|
||||
case IDENTIFIER:
|
||||
s.Insert(lit)
|
||||
tok2, lit2 := p.lookahead(Values)
|
||||
switch tok2 {
|
||||
case COMMA:
|
||||
continue
|
||||
case CPAR:
|
||||
return s, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Found '%s', expected ',' or ')'", lit2)
|
||||
}
|
||||
case COMMA: // handled here since we can have "(,"
|
||||
if s.Len() == 0 {
|
||||
s.Insert("") // to handle (,
|
||||
}
|
||||
tok2, _ := p.lookahead(Values)
|
||||
if tok2 == CPAR {
|
||||
s.Insert("") // to handle ,) Double "" removed by StringSet
|
||||
return s, nil
|
||||
}
|
||||
if tok2 == COMMA {
|
||||
p.consume(Values)
|
||||
s.Insert("") // to handle ,, Double "" removed by StringSet
|
||||
}
|
||||
default: // it can be operator
|
||||
return s, fmt.Errorf("Found '%s', expected ',', or identifier", lit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse the only value for exact match style
|
||||
func (p *Parser) parseExactValue() (util.StringSet, error) {
|
||||
s := util.NewStringSet()
|
||||
if tok, lit := p.consume(Values); tok == IDENTIFIER {
|
||||
s.Insert(lit)
|
||||
} else {
|
||||
return nil, fmt.Errorf("Found '%s', expected identifier", lit)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Parse takes a string representing a selector and returns a selector
|
||||
@ -303,24 +706,18 @@ func (lsel *LabelSelector) String() (string, error) {
|
||||
// as they parse different selectors with different syntaxes.
|
||||
// The input will cause an error if it does not follow this form:
|
||||
//
|
||||
// <selector-syntax> ::= <requirement> | <requirement> "," <selector-syntax>
|
||||
// <requirement> ::= WHITESPACE_OPT KEY <set-restriction>
|
||||
// <set-restriction> ::= "" | <inclusion-exclusion> <value-set>
|
||||
// <selector-syntax> ::= <requirement> | <requirement> "," <selector-syntax> ]
|
||||
// <requirement> ::= KEY [ <set-based-restriction> | <exact-match-restriction>
|
||||
// <set-based-restriction> ::= "" | <inclusion-exclusion> <value-set>
|
||||
// <inclusion-exclusion> ::= <inclusion> | <exclusion>
|
||||
// <exclusion> ::= WHITESPACE "not" <inclusion>
|
||||
// <inclusion> ::= WHITESPACE "in" WHITESPACE
|
||||
// <exclusion> ::= "not" <inclusion>
|
||||
// <inclusion> ::= "in"
|
||||
// <value-set> ::= "(" <values> ")"
|
||||
// <values> ::= VALUE | VALUE "," <values>
|
||||
//
|
||||
// KEY is a sequence of one or more characters that does not contain ',' or ' '
|
||||
// [^, ]+
|
||||
// VALUE is a sequence of zero or more characters that does not contain ',', ' ' or ')'
|
||||
// [^, )]*
|
||||
// WHITESPACE_OPT is a sequence of zero or more whitespace characters
|
||||
// \s*
|
||||
// WHITESPACE is a sequence of one or more whitespace characters
|
||||
// \s+
|
||||
//
|
||||
// <exact-match-restriction> ::= ["="|"=="|"!="] VALUE
|
||||
// KEY is a sequence of one or more characters following [ DNS_SUBDOMAIN "/" ] DNS_LABEL
|
||||
// VALUE is a sequence of zero or more characters "([A-Za-z0-9_-\.])". Max length is 64 character.
|
||||
// Delimiter is white space: (' ', '\t')
|
||||
// Example of valid syntax:
|
||||
// "x in (foo,,baz),y,z not in ()"
|
||||
//
|
||||
@ -333,115 +730,28 @@ func (lsel *LabelSelector) String() (string, error) {
|
||||
// (4) A requirement with just a KEY - as in "y" above - denotes that
|
||||
// the KEY exists and can be any VALUE.
|
||||
//
|
||||
// TODO: value validation possibly including duplicate value check, restricting certain characters
|
||||
func Parse(selector string) (SetBasedSelector, error) {
|
||||
var items []Requirement
|
||||
var key string
|
||||
var op Operator
|
||||
var vals util.StringSet
|
||||
const (
|
||||
startReq int = iota
|
||||
inKey
|
||||
waitOp
|
||||
inVals
|
||||
)
|
||||
const pos = "position %d:%s"
|
||||
inRegex, errIn := regexp.Compile("^\\s*in\\s+\\(")
|
||||
if errIn != nil {
|
||||
return nil, errIn
|
||||
func Parse(selector string) (Selector, error) {
|
||||
p := &Parser{l: &Lexer{s: selector, pos: 0}}
|
||||
items, error := p.parse()
|
||||
if error == nil {
|
||||
sort.Sort(ByKey(items)) // sort to grant determistic parsing
|
||||
return &LabelSelector{Requirements: items}, error
|
||||
}
|
||||
notInRegex, errNotIn := regexp.Compile("^\\s*not\\s+in\\s+\\(")
|
||||
if errNotIn != nil {
|
||||
return nil, errNotIn
|
||||
}
|
||||
|
||||
state := startReq
|
||||
strStart := 0
|
||||
for i := 0; i < len(selector); i++ {
|
||||
switch state {
|
||||
case startReq:
|
||||
switch selector[i] {
|
||||
case ',':
|
||||
return nil, fmt.Errorf("a requirement can't be empty. "+pos, i, selector)
|
||||
case ' ', '\t', '\n', '\f', '\r':
|
||||
default:
|
||||
state = inKey
|
||||
strStart = i
|
||||
}
|
||||
case inKey:
|
||||
switch selector[i] {
|
||||
case ',':
|
||||
state = startReq
|
||||
if req, err := NewRequirement(selector[strStart:i], Exists, nil); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
items = append(items, *req)
|
||||
}
|
||||
case ' ', '\t', '\n', '\f', '\r':
|
||||
state = waitOp
|
||||
key = selector[strStart:i]
|
||||
}
|
||||
case waitOp:
|
||||
if loc := inRegex.FindStringIndex(selector[i:]); loc != nil {
|
||||
op = In
|
||||
i += loc[1] - loc[0] - 1
|
||||
} else if loc = notInRegex.FindStringIndex(selector[i:]); loc != nil {
|
||||
op = NotIn
|
||||
i += loc[1] - loc[0] - 1
|
||||
} else {
|
||||
return nil, fmt.Errorf("expected \" in (\"/\" not in (\" after key. "+pos, i, selector)
|
||||
}
|
||||
state = inVals
|
||||
vals = util.NewStringSet()
|
||||
strStart = i + 1
|
||||
case inVals:
|
||||
switch selector[i] {
|
||||
case ',':
|
||||
vals.Insert(selector[strStart:i])
|
||||
strStart = i + 1
|
||||
case ' ':
|
||||
return nil, fmt.Errorf("white space not allowed in set strings. "+pos, i, selector)
|
||||
case ')':
|
||||
if i+1 == len(selector)-1 && selector[i+1] == ',' {
|
||||
return nil, fmt.Errorf("expected requirement after comma. "+pos, i+1, selector)
|
||||
}
|
||||
if i+1 < len(selector) && selector[i+1] != ',' {
|
||||
return nil, fmt.Errorf("requirements must be comma-separated. "+pos, i+1, selector)
|
||||
}
|
||||
state = startReq
|
||||
vals.Insert(selector[strStart:i])
|
||||
if req, err := NewRequirement(key, op, vals); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
items = append(items, *req)
|
||||
}
|
||||
if i+1 < len(selector) {
|
||||
i += 1 //advance past comma
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch state {
|
||||
case inKey:
|
||||
if req, err := NewRequirement(selector[strStart:], Exists, nil); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
items = append(items, *req)
|
||||
}
|
||||
case waitOp:
|
||||
return nil, fmt.Errorf("input terminated while waiting for operator \"in \"/\"not in \":%s", selector)
|
||||
case inVals:
|
||||
return nil, fmt.Errorf("input terminated while waiting for value set:%s", selector)
|
||||
}
|
||||
|
||||
return &LabelSelector{Requirements: items}, nil
|
||||
return nil, error
|
||||
}
|
||||
|
||||
// TODO: unify with validation.validateLabels
|
||||
const qualifiedNameErrorMsg string = "must match regex [" + util.DNS1123SubdomainFmt + " / ] " + util.DNS1123LabelFmt
|
||||
|
||||
func validateLabelKey(k string) error {
|
||||
if !util.IsDNSLabel(k) {
|
||||
return errors.NewFieldNotSupported("key", k)
|
||||
if !util.IsQualifiedName(k) {
|
||||
return errors.NewFieldInvalid("label key", k, qualifiedNameErrorMsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLabelValue(v string) error {
|
||||
if !util.IsValidLabelValue(v) {
|
||||
return errors.NewFieldInvalid("label value", v, qualifiedNameErrorMsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -470,6 +780,23 @@ func SelectorFromSet(ls Set) Selector {
|
||||
return andTerm(items)
|
||||
}
|
||||
|
||||
// SelectorFromSet returns a Selector which will match exactly the given Set. A
|
||||
// nil Set is considered equivalent to Everything().
|
||||
func SelectorFromSetParse(ls Set) (Selector, error) {
|
||||
if ls == nil {
|
||||
return LabelSelector{}, nil
|
||||
}
|
||||
var requirements []Requirement
|
||||
for label, value := range ls {
|
||||
if r, err := NewRequirement(label, InOperator, util.NewStringSet(value)); err != nil {
|
||||
return LabelSelector{}, err
|
||||
} else {
|
||||
requirements = append(requirements, *r)
|
||||
}
|
||||
}
|
||||
return LabelSelector{Requirements: requirements}, nil
|
||||
}
|
||||
|
||||
// ParseSelector takes a string representing a selector and returns an
|
||||
// object suitable for matching, or an error.
|
||||
func ParseSelector(selector string) (Selector, error) {
|
||||
@ -501,3 +828,12 @@ func ParseSelector(selector string) (Selector, error) {
|
||||
func OneTermEqualSelector(k, v string) Selector {
|
||||
return &hasTerm{label: k, value: v}
|
||||
}
|
||||
|
||||
// OneTermEqualSelectorParse: implement OneTermEqualSelector using of LabelSelector and Requirement
|
||||
// TODO: remove the original OneTermSelector and rename OneTermEqualSelectorParse to OneTermEqualSelector
|
||||
// Since OneTermEqualSelector cannot return an error. the Requirement based version ignore error.
|
||||
// it's up to the caller being sure that k and v are not empty
|
||||
func OneTermEqualSelectorParse(k, v string) Selector {
|
||||
r, _ := NewRequirement(k, InOperator, util.NewStringSet(v))
|
||||
return &LabelSelector{Requirements: []Requirement{*r}}
|
||||
}
|
||||
|
@ -42,12 +42,23 @@ func TestSelectorParse(t *testing.T) {
|
||||
if test != lq.String() {
|
||||
t.Errorf("%v restring gave: %v\n", test, lq.String())
|
||||
}
|
||||
lq, err = Parse(test)
|
||||
if err != nil {
|
||||
t.Errorf("%v: error %v (%#v)\n", test, err, err)
|
||||
}
|
||||
if test != lq.String() {
|
||||
t.Errorf("%v restring gave: %v\n", test, lq.String())
|
||||
}
|
||||
}
|
||||
for _, test := range testBadStrings {
|
||||
_, err := ParseSelector(test)
|
||||
if err == nil {
|
||||
t.Errorf("%v: did not get expected error\n", test)
|
||||
}
|
||||
_, err = Parse(test)
|
||||
if err == nil {
|
||||
t.Errorf("%v: did not get expected error\n", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,6 +71,14 @@ func TestDeterministicParse(t *testing.T) {
|
||||
if s1.String() != s2.String() {
|
||||
t.Errorf("Non-deterministic parse")
|
||||
}
|
||||
s1, err = Parse("x=a,a=x")
|
||||
s2, err2 = Parse("a=x,x=a")
|
||||
if err != nil || err2 != nil {
|
||||
t.Errorf("Unexpected parse error")
|
||||
}
|
||||
if s1.String() != s2.String() {
|
||||
t.Errorf("Non-deterministic parse")
|
||||
}
|
||||
}
|
||||
|
||||
func expectMatch(t *testing.T, selector string, ls Set) {
|
||||
@ -71,6 +90,14 @@ func expectMatch(t *testing.T, selector string, ls Set) {
|
||||
if !lq.Matches(ls) {
|
||||
t.Errorf("Wanted %s to match '%s', but it did not.\n", selector, ls)
|
||||
}
|
||||
lq, err = Parse(selector)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to parse %v as a selector\n", selector)
|
||||
return
|
||||
}
|
||||
if !lq.Matches(ls) {
|
||||
t.Errorf("Wanted %s to match '%s', but it did not.\n", selector, ls)
|
||||
}
|
||||
}
|
||||
|
||||
func expectNoMatch(t *testing.T, selector string, ls Set) {
|
||||
@ -82,6 +109,14 @@ func expectNoMatch(t *testing.T, selector string, ls Set) {
|
||||
if lq.Matches(ls) {
|
||||
t.Errorf("Wanted '%s' to not match '%s', but it did.", selector, ls)
|
||||
}
|
||||
lq, err = Parse(selector)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to parse %v as a selector\n", selector)
|
||||
return
|
||||
}
|
||||
if lq.Matches(ls) {
|
||||
t.Errorf("Wanted '%s' to not match '%s', but it did.", selector, ls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEverything(t *testing.T) {
|
||||
@ -98,6 +133,7 @@ func TestSelectorMatches(t *testing.T) {
|
||||
expectMatch(t, "x=y", Set{"x": "y"})
|
||||
expectMatch(t, "x=y,z=w", Set{"x": "y", "z": "w"})
|
||||
expectMatch(t, "x!=y,z!=w", Set{"x": "z", "z": "a"})
|
||||
expectMatch(t, "notin=in", Set{"notin": "in"}) // in and notin in exactMatch
|
||||
expectNoMatch(t, "x=y", Set{"x": "z"})
|
||||
expectNoMatch(t, "x=y,z=w", Set{"x": "w", "z": "w"})
|
||||
expectNoMatch(t, "x!=y,z!=w", Set{"x": "z", "z": "w"})
|
||||
@ -123,16 +159,33 @@ func TestOneTermEqualSelector(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOneTermEqualSelectorParse(t *testing.T) {
|
||||
if !OneTermEqualSelectorParse("x", "y").Matches(Set{"x": "y"}) {
|
||||
t.Errorf("No match when match expected.")
|
||||
}
|
||||
if OneTermEqualSelectorParse("x", "y").Matches(Set{"x": "z"}) {
|
||||
t.Errorf("Match when none expected.")
|
||||
}
|
||||
}
|
||||
|
||||
func expectMatchDirect(t *testing.T, selector, ls Set) {
|
||||
if !SelectorFromSet(selector).Matches(ls) {
|
||||
t.Errorf("Wanted %s to match '%s', but it did not.\n", selector, ls)
|
||||
}
|
||||
s, e := SelectorFromSetParse(selector)
|
||||
if e == nil && !s.Matches(ls) {
|
||||
t.Errorf("Wanted '%s' to match '%s', but it did not.\n", selector, ls)
|
||||
}
|
||||
}
|
||||
|
||||
func expectNoMatchDirect(t *testing.T, selector, ls Set) {
|
||||
if SelectorFromSet(selector).Matches(ls) {
|
||||
t.Errorf("Wanted '%s' to not match '%s', but it did.", selector, ls)
|
||||
}
|
||||
s, e := SelectorFromSetParse(selector)
|
||||
if e == nil && s.Matches(ls) {
|
||||
t.Errorf("Wanted '%s' to not match '%s', but it did.", selector, ls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMatches(t *testing.T) {
|
||||
@ -210,6 +263,146 @@ func TestRequiresExactMatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiresExactMatchParse(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
S Selector
|
||||
Label string
|
||||
Value string
|
||||
Found bool
|
||||
}{
|
||||
"empty set": {Set{}.AsSelector(), "test", "", false},
|
||||
"empty hasTerm": {&LabelSelector{}, "test", "", false},
|
||||
"skipped Requirement": {&LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("a", InOperator, util.NewStringSet("b"), t)}}, "test", "", false},
|
||||
"valid Requirement": {&LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("test", InOperator, util.NewStringSet("b"), t)}}, "test", "b", true},
|
||||
"valid Requirement no value": {&LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("test", InOperator, util.NewStringSet(""), t)}}, "test", "", true},
|
||||
"valid Requirement NotIn": {&LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("test", NotInOperator, util.NewStringSet("b"), t)}}, "test", "", false},
|
||||
"valid notHasTerm no value": {&LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("test", NotInOperator, util.NewStringSet(""), t)}}, "test", "", false},
|
||||
"2 Requirements with non-match": {&LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("test", ExistsOperator, util.NewStringSet("b"), t),
|
||||
getRequirement("test", InOperator, util.NewStringSet("b"), t)}}, "test", "b", true},
|
||||
}
|
||||
for k, v := range testCases {
|
||||
value, found := v.S.RequiresExactMatch(v.Label)
|
||||
if value != v.Value {
|
||||
t.Errorf("%s: expected value %s, got %s", k, v.Value, value)
|
||||
}
|
||||
if found != v.Found {
|
||||
t.Errorf("%s: expected found %t, got %t", k, v.Found, found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLexer(t *testing.T) {
|
||||
testcases := []struct {
|
||||
s string
|
||||
t Token
|
||||
}{
|
||||
{"", EOS},
|
||||
{",", COMMA},
|
||||
{"notin", NOTIN},
|
||||
{"in", IN},
|
||||
{"=", EQUAL},
|
||||
{"==", EEQUAL},
|
||||
{"!=", NEQUAL},
|
||||
{"(", OPAR},
|
||||
{")", CPAR},
|
||||
{"||", IDENTIFIER},
|
||||
{"!", ERROR},
|
||||
}
|
||||
for _, v := range testcases {
|
||||
l := &Lexer{s: v.s, pos: 0}
|
||||
token, lit := l.Lex()
|
||||
if token != v.t {
|
||||
t.Errorf("Got %d it should be %d for '%s'", token, v.t, v.s)
|
||||
}
|
||||
if v.t != ERROR && lit != v.s {
|
||||
t.Errorf("Got '%s' it should be '%s'", lit, v.s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func min(l, r int) (m int) {
|
||||
m = r
|
||||
if l < r {
|
||||
m = l
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestLexerSequence(t *testing.T) {
|
||||
testcases := []struct {
|
||||
s string
|
||||
t []Token
|
||||
}{
|
||||
{"key in ( value )", []Token{IDENTIFIER, IN, OPAR, IDENTIFIER, CPAR}},
|
||||
{"key notin ( value )", []Token{IDENTIFIER, NOTIN, OPAR, IDENTIFIER, CPAR}},
|
||||
{"key in ( value1, value2 )", []Token{IDENTIFIER, IN, OPAR, IDENTIFIER, COMMA, IDENTIFIER, CPAR}},
|
||||
{"key", []Token{IDENTIFIER}},
|
||||
{"()", []Token{OPAR, CPAR}},
|
||||
{"x in (),y", []Token{IDENTIFIER, IN, OPAR, CPAR, COMMA, IDENTIFIER}},
|
||||
{"== != (), = notin", []Token{EEQUAL, NEQUAL, OPAR, CPAR, COMMA, EQUAL, NOTIN}},
|
||||
}
|
||||
for _, v := range testcases {
|
||||
var literals []string
|
||||
var tokens []Token
|
||||
l := &Lexer{s: v.s, pos: 0}
|
||||
for {
|
||||
token, lit := l.Lex()
|
||||
if token == EOS {
|
||||
break
|
||||
}
|
||||
tokens = append(tokens, token)
|
||||
literals = append(literals, lit)
|
||||
}
|
||||
if len(tokens) != len(v.t) {
|
||||
t.Errorf("Bad number of tokens for '%s %d, %d", v.s, len(tokens), len(v.t))
|
||||
}
|
||||
for i := 0; i < min(len(tokens), len(v.t)); i++ {
|
||||
if tokens[i] != v.t[i] {
|
||||
t.Errorf("Test '%s': Mismatching in token type found '%s' it should be '%s'", v.s, tokens[i], v.t[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestParserLookahead(t *testing.T) {
|
||||
testcases := []struct {
|
||||
s string
|
||||
t []Token
|
||||
}{
|
||||
{"key in ( value )", []Token{IDENTIFIER, IN, OPAR, IDENTIFIER, CPAR, EOS}},
|
||||
{"key notin ( value )", []Token{IDENTIFIER, NOTIN, OPAR, IDENTIFIER, CPAR, EOS}},
|
||||
{"key in ( value1, value2 )", []Token{IDENTIFIER, IN, OPAR, IDENTIFIER, COMMA, IDENTIFIER, CPAR, EOS}},
|
||||
{"key", []Token{IDENTIFIER, EOS}},
|
||||
{"()", []Token{OPAR, CPAR, EOS}},
|
||||
{"", []Token{EOS}},
|
||||
{"x in (),y", []Token{IDENTIFIER, IN, OPAR, CPAR, COMMA, IDENTIFIER, EOS}},
|
||||
{"== != (), = notin", []Token{EEQUAL, NEQUAL, OPAR, CPAR, COMMA, EQUAL, NOTIN, EOS}},
|
||||
}
|
||||
for _, v := range testcases {
|
||||
p := &Parser{l: &Lexer{s: v.s, pos: 0}, position: 0}
|
||||
p.scan()
|
||||
if len(p.scannedItems) != len(v.t) {
|
||||
t.Errorf("Expected %d items found %d", len(v.t), len(p.scannedItems))
|
||||
}
|
||||
for {
|
||||
token, lit := p.lookahead(KeyAndOperator)
|
||||
|
||||
token2, lit2 := p.consume(KeyAndOperator)
|
||||
if token == EOS {
|
||||
break
|
||||
}
|
||||
if token != token2 || lit != lit2 {
|
||||
t.Errorf("Bad values")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequirementConstructor(t *testing.T) {
|
||||
requirementConstructorTests := []struct {
|
||||
Key string
|
||||
@ -217,15 +410,14 @@ func TestRequirementConstructor(t *testing.T) {
|
||||
Vals util.StringSet
|
||||
Success bool
|
||||
}{
|
||||
{"x", 8, util.NewStringSet("foo"), false},
|
||||
{"x", In, nil, false},
|
||||
{"x", NotIn, util.NewStringSet(), false},
|
||||
{"x", In, util.NewStringSet("foo"), true},
|
||||
{"x", NotIn, util.NewStringSet("foo"), true},
|
||||
{"x", Exists, nil, true},
|
||||
{"1foo", In, util.NewStringSet("bar"), true},
|
||||
{"1234", In, util.NewStringSet("bar"), true},
|
||||
{strings.Repeat("a", 64), Exists, nil, false}, //breaks DNS rule that len(key) <= 63
|
||||
{"x", InOperator, nil, false},
|
||||
{"x", NotInOperator, util.NewStringSet(), false},
|
||||
{"x", InOperator, util.NewStringSet("foo"), true},
|
||||
{"x", NotInOperator, util.NewStringSet("foo"), true},
|
||||
{"x", ExistsOperator, nil, true},
|
||||
{"1foo", InOperator, util.NewStringSet("bar"), true},
|
||||
{"1234", InOperator, util.NewStringSet("bar"), true},
|
||||
{strings.Repeat("a", 64), ExistsOperator, nil, false}, //breaks DNS rule that len(key) <= 63
|
||||
}
|
||||
for _, rc := range requirementConstructorTests {
|
||||
if _, err := NewRequirement(rc.Key, rc.Op, rc.Vals); err == nil && !rc.Success {
|
||||
@ -244,25 +436,30 @@ func TestToString(t *testing.T) {
|
||||
Valid bool
|
||||
}{
|
||||
{&LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", In, util.NewStringSet("abc", "def"), t),
|
||||
getRequirement("y", NotIn, util.NewStringSet("jkl"), t),
|
||||
getRequirement("z", Exists, nil, t),
|
||||
}}, "x in (abc,def),y not in (jkl),z", true},
|
||||
getRequirement("x", InOperator, util.NewStringSet("abc", "def"), t),
|
||||
getRequirement("y", NotInOperator, util.NewStringSet("jkl"), t),
|
||||
getRequirement("z", ExistsOperator, nil, t),
|
||||
}}, "x in (abc,def),y notin (jkl),z", true},
|
||||
{&LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", In, util.NewStringSet("abc", "def"), t),
|
||||
getRequirement("x", InOperator, util.NewStringSet("abc", "def"), t),
|
||||
req,
|
||||
}}, "", false},
|
||||
}}, "x in (abc,def),", false},
|
||||
{&LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", NotIn, util.NewStringSet("abc"), t),
|
||||
getRequirement("y", In, util.NewStringSet("jkl", "mno"), t),
|
||||
getRequirement("z", NotIn, util.NewStringSet(""), t),
|
||||
}}, "x not in (abc),y in (jkl,mno),z not in ()", true},
|
||||
getRequirement("x", NotInOperator, util.NewStringSet("abc"), t),
|
||||
getRequirement("y", InOperator, util.NewStringSet("jkl", "mno"), t),
|
||||
getRequirement("z", NotInOperator, util.NewStringSet(""), t),
|
||||
}}, "x notin (abc),y in (jkl,mno),z notin ()", true},
|
||||
{&LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", EqualsOperator, util.NewStringSet("abc"), t),
|
||||
getRequirement("y", DoubleEqualsOperator, util.NewStringSet("jkl"), t),
|
||||
getRequirement("z", NotEqualsOperator, util.NewStringSet("a"), t),
|
||||
}}, "x=abc,y==jkl,z!=a", true},
|
||||
}
|
||||
for _, ts := range toStringTests {
|
||||
if out, err := ts.In.String(); err != nil && ts.Valid {
|
||||
t.Errorf("%+v.String() => %v, expected no error", ts.In, err)
|
||||
if out := ts.In.String(); out == "" && ts.Valid {
|
||||
t.Errorf("%+v.String() => '%v' expected no error", ts.In)
|
||||
} else if out != ts.Out {
|
||||
t.Errorf("%+v.String() => %v, want %v", ts.In, out, ts.Out)
|
||||
t.Errorf("%+v.String() => '%v' want '%v'", ts.In, out, ts.Out)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -273,31 +470,28 @@ func TestRequirementLabelSelectorMatching(t *testing.T) {
|
||||
Set Set
|
||||
Sel *LabelSelector
|
||||
Match bool
|
||||
Valid bool
|
||||
}{
|
||||
{Set{"x": "foo", "y": "baz"}, &LabelSelector{Requirements: []Requirement{
|
||||
req,
|
||||
}}, false, false},
|
||||
}}, false},
|
||||
{Set{"x": "foo", "y": "baz"}, &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", In, util.NewStringSet("foo"), t),
|
||||
getRequirement("y", NotIn, util.NewStringSet("alpha"), t),
|
||||
}}, true, true},
|
||||
getRequirement("x", InOperator, util.NewStringSet("foo"), t),
|
||||
getRequirement("y", NotInOperator, util.NewStringSet("alpha"), t),
|
||||
}}, true},
|
||||
{Set{"x": "foo", "y": "baz"}, &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", In, util.NewStringSet("foo"), t),
|
||||
getRequirement("y", In, util.NewStringSet("alpha"), t),
|
||||
}}, false, true},
|
||||
getRequirement("x", InOperator, util.NewStringSet("foo"), t),
|
||||
getRequirement("y", InOperator, util.NewStringSet("alpha"), t),
|
||||
}}, false},
|
||||
{Set{"y": ""}, &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", NotIn, util.NewStringSet(""), t),
|
||||
getRequirement("y", Exists, nil, t),
|
||||
}}, true, true},
|
||||
getRequirement("x", NotInOperator, util.NewStringSet(""), t),
|
||||
getRequirement("y", ExistsOperator, nil, t),
|
||||
}}, true},
|
||||
{Set{"y": "baz"}, &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", In, util.NewStringSet(""), t),
|
||||
}}, false, true},
|
||||
getRequirement("x", InOperator, util.NewStringSet(""), t),
|
||||
}}, false},
|
||||
}
|
||||
for _, lsm := range labelSelectorMatchingTests {
|
||||
if match, err := lsm.Sel.Matches(lsm.Set); err != nil && lsm.Valid {
|
||||
t.Errorf("%+v.Matches(%#v) => %v, expected no error", lsm.Sel, lsm.Set, err)
|
||||
} else if match != lsm.Match {
|
||||
if match := lsm.Sel.Matches(lsm.Set); match != lsm.Match {
|
||||
t.Errorf("%+v.Matches(%#v) => %v, want %v", lsm.Sel, lsm.Set, match, lsm.Match)
|
||||
}
|
||||
}
|
||||
@ -306,59 +500,95 @@ func TestRequirementLabelSelectorMatching(t *testing.T) {
|
||||
func TestSetSelectorParser(t *testing.T) {
|
||||
setSelectorParserTests := []struct {
|
||||
In string
|
||||
Out SetBasedSelector
|
||||
Out Selector
|
||||
Match bool
|
||||
Valid bool
|
||||
}{
|
||||
{"", &LabelSelector{Requirements: nil}, true, true},
|
||||
{"\rx", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", Exists, nil, t),
|
||||
getRequirement("x", ExistsOperator, nil, t),
|
||||
}}, true, true},
|
||||
{"this-is-a-dns.domain.com/key-with-dash", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("this-is-a-dns.domain.com/key-with-dash", ExistsOperator, nil, t),
|
||||
}}, true, true},
|
||||
{"this-is-another-dns.domain.com/key-with-dash in (so,what)", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("this-is-another-dns.domain.com/key-with-dash", InOperator, util.NewStringSet("so", "what"), t),
|
||||
}}, true, true},
|
||||
{"0.1.2.domain/99 notin (10.10.100.1, tick.tack.clock)", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("0.1.2.domain/99", NotInOperator, util.NewStringSet("10.10.100.1", "tick.tack.clock"), t),
|
||||
}}, true, true},
|
||||
{"foo in (abc)", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("foo", In, util.NewStringSet("abc"), t),
|
||||
getRequirement("foo", InOperator, util.NewStringSet("abc"), t),
|
||||
}}, true, true},
|
||||
{"x not\n\tin (abc)", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", NotIn, util.NewStringSet("abc"), t),
|
||||
{"x notin\n (abc)", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", NotInOperator, util.NewStringSet("abc"), t),
|
||||
}}, true, true},
|
||||
{"x not in \t (abc,def)", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", NotIn, util.NewStringSet("abc", "def"), t),
|
||||
{"x notin \t (abc,def)", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", NotInOperator, util.NewStringSet("abc", "def"), t),
|
||||
}}, true, true},
|
||||
{"x in (abc,def)", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", In, util.NewStringSet("abc", "def"), t),
|
||||
getRequirement("x", InOperator, util.NewStringSet("abc", "def"), t),
|
||||
}}, true, true},
|
||||
{"x in (abc,)", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", In, util.NewStringSet("abc", ""), t),
|
||||
getRequirement("x", InOperator, util.NewStringSet("abc", ""), t),
|
||||
}}, true, true},
|
||||
{"x in ()", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", In, util.NewStringSet(""), t),
|
||||
getRequirement("x", InOperator, util.NewStringSet(""), t),
|
||||
}}, true, true},
|
||||
{"x not in (abc,,def),bar,z in (),w", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", NotIn, util.NewStringSet("abc", "", "def"), t),
|
||||
getRequirement("bar", Exists, nil, t),
|
||||
getRequirement("z", In, util.NewStringSet(""), t),
|
||||
getRequirement("w", Exists, nil, t),
|
||||
{"x notin (abc,,def),bar,z in (),w", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("bar", ExistsOperator, nil, t),
|
||||
getRequirement("w", ExistsOperator, nil, t),
|
||||
getRequirement("x", NotInOperator, util.NewStringSet("abc", "", "def"), t),
|
||||
getRequirement("z", InOperator, util.NewStringSet(""), t),
|
||||
}}, true, true},
|
||||
{"x,y in (a)", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("y", In, util.NewStringSet("a"), t),
|
||||
getRequirement("x", Exists, nil, t),
|
||||
getRequirement("y", InOperator, util.NewStringSet("a"), t),
|
||||
getRequirement("x", ExistsOperator, nil, t),
|
||||
}}, false, true},
|
||||
{"x=a", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", EqualsOperator, util.NewStringSet("a"), t),
|
||||
}}, true, true},
|
||||
{"x=a,y!=b", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", EqualsOperator, util.NewStringSet("a"), t),
|
||||
getRequirement("y", NotEqualsOperator, util.NewStringSet("b"), t),
|
||||
}}, true, true},
|
||||
{"x=a,y!=b,z in (h,i,j)", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", EqualsOperator, util.NewStringSet("a"), t),
|
||||
getRequirement("y", NotEqualsOperator, util.NewStringSet("b"), t),
|
||||
getRequirement("z", InOperator, util.NewStringSet("h", "i", "j"), t),
|
||||
}}, true, true},
|
||||
{"x=a||y=b", &LabelSelector{Requirements: []Requirement{}}, false, false},
|
||||
{"x,,y", nil, true, false},
|
||||
{",x,y", nil, true, false},
|
||||
{"x nott in (y)", nil, true, false},
|
||||
{"x not in ( )", nil, true, false},
|
||||
{"x not in (, a)", nil, true, false},
|
||||
{"x notin ( )", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("x", NotInOperator, util.NewStringSet(""), t),
|
||||
}}, true, true},
|
||||
{"x notin (, a)", &LabelSelector{Requirements: []Requirement{
|
||||
|
||||
getRequirement("x", NotInOperator, util.NewStringSet("", "a"), t),
|
||||
}}, true, true},
|
||||
{"a in (xyz),", nil, true, false},
|
||||
{"a in (xyz)b not in ()", nil, true, false},
|
||||
{"a ", nil, true, false},
|
||||
{"a not in(", nil, true, false},
|
||||
{"a in (xyz)b notin ()", nil, true, false},
|
||||
{"a ", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("a", ExistsOperator, nil, t),
|
||||
}}, true, true},
|
||||
{"a in (x,y,notin, z,in)", &LabelSelector{Requirements: []Requirement{
|
||||
getRequirement("a", InOperator, util.NewStringSet("in", "notin", "x", "y", "z"), t),
|
||||
}}, true, true}, // operator 'in' inside list of identifiers
|
||||
{"a in (xyz abc)", nil, false, false}, // no comma
|
||||
{"a notin(", nil, true, false}, // bad formed
|
||||
{"a (", nil, false, false}, // cpar
|
||||
{"(", nil, false, false}, // opar
|
||||
}
|
||||
|
||||
for _, ssp := range setSelectorParserTests {
|
||||
if sel, err := Parse(ssp.In); err != nil && ssp.Valid {
|
||||
t.Errorf("Parse(%s) => %v expected no error", ssp.In, err)
|
||||
} else if err == nil && !ssp.Valid {
|
||||
t.Errorf("Parse(%s) => %+v expected error", ssp.In, sel)
|
||||
} else if ssp.Match && !reflect.DeepEqual(sel, ssp.Out) {
|
||||
t.Errorf("parse output %+v doesn't match %+v, expected match", sel, ssp.Out)
|
||||
t.Errorf("Parse(%s) => parse output %+v doesn't match %+v, expected match", ssp.In, sel, ssp.Out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,3 +103,13 @@ func IsQualifiedName(value string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const LabelValueFmt string = "([A-Za-z0-9_\\-\\\\.]*)"
|
||||
|
||||
var labelValueRegexp = regexp.MustCompile("^" + LabelValueFmt + "$")
|
||||
|
||||
const labelValueMaxLength int = 63
|
||||
|
||||
func IsValidLabelValue(value string) bool {
|
||||
return (len(value) <= labelValueMaxLength && labelValueRegexp.MatchString(value))
|
||||
}
|
||||
|
@ -189,3 +189,35 @@ func TestIsQualifiedName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidLabelValue(t *testing.T) {
|
||||
successCases := []string{
|
||||
"simple",
|
||||
"now-with-dashes",
|
||||
"1-starts-with-num",
|
||||
"end-with-num-1",
|
||||
"-starts-with-dash",
|
||||
"ends-with-dash-",
|
||||
".starts.with.dot",
|
||||
"ends.with.dot.",
|
||||
"\\preserve\\backslash",
|
||||
"1234", // only num
|
||||
strings.Repeat("a", 63), // to the limit
|
||||
}
|
||||
for i := range successCases {
|
||||
if !IsValidLabelValue(successCases[i]) {
|
||||
t.Errorf("case[%d] expected success", i)
|
||||
}
|
||||
}
|
||||
|
||||
errorCases := []string{
|
||||
"nospecialchars%^=@",
|
||||
"Tama-nui-te-rā.is.Māori.sun",
|
||||
strings.Repeat("a", 65),
|
||||
}
|
||||
for i := range errorCases {
|
||||
if IsValidLabelValue(errorCases[i]) {
|
||||
t.Errorf("case[%d] expected failure", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user