Extending label.Parse method to support exact match

This commit is contained in:
Salvatore Dario Minonne 2015-02-15 00:07:30 +01:00
parent 86434b4038
commit 0186acc37c
4 changed files with 854 additions and 246 deletions

View File

@ -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}}
}

View File

@ -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)
}
}
}

View File

@ -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))
}

View File

@ -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)
}
}
}