diff --git a/pkg/labels/selector.go b/pkg/labels/selector.go index 2a3ecbc7b21..eba8b708d29 100644 --- a/pkg/labels/selector.go +++ b/pkg/labels/selector.go @@ -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 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: // -// ::= | "," -// ::= WHITESPACE_OPT KEY -// ::= "" | +// ::= | "," ] +// ::= KEY [ | +// ::= "" | // ::= | -// ::= WHITESPACE "not" -// ::= WHITESPACE "in" WHITESPACE +// ::= "not" +// ::= "in" // ::= "(" ")" // ::= VALUE | VALUE "," -// -// 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+ -// +// ::= ["="|"=="|"!="] 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}} +} diff --git a/pkg/labels/selector_test.go b/pkg/labels/selector_test.go index 07b08d02b6c..70a232b3e2f 100644 --- a/pkg/labels/selector_test.go +++ b/pkg/labels/selector_test.go @@ -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) } } } diff --git a/pkg/util/validation.go b/pkg/util/validation.go index 5b1143e3c87..f366393a25f 100644 --- a/pkg/util/validation.go +++ b/pkg/util/validation.go @@ -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)) +} diff --git a/pkg/util/validation_test.go b/pkg/util/validation_test.go index 556e071af1a..47222388c01 100644 --- a/pkg/util/validation_test.go +++ b/pkg/util/validation_test.go @@ -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) + } + } +}