mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 17:30:00 +00:00
state-based parser for multi-valued set selector syntax
This commit is contained in:
parent
3155cad475
commit
444b74302a
@ -22,6 +22,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -137,6 +138,16 @@ func (t andTerm) String() string {
|
|||||||
return strings.Join(terms, ",")
|
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
|
// Operator represents a key's relationship
|
||||||
// to a set of values in a Requirement.
|
// to a set of values in a Requirement.
|
||||||
type Operator int
|
type Operator int
|
||||||
@ -171,13 +182,18 @@ type Requirement struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRequirement is the constructor for a Requirement.
|
// NewRequirement is the constructor for a Requirement.
|
||||||
// If either of these rules is violated, an error is returned:
|
// If any of these rules is violated, an error is returned:
|
||||||
// (1) The operator can only be In, NotIn or Exists.
|
// (1) The operator can only be In, NotIn or Exists.
|
||||||
// (2) If the operator is In or NotIn, the values set must
|
// (2) If the operator is In or NotIn, the values set must
|
||||||
// be non-empty.
|
// be non-empty.
|
||||||
|
// (3) The key is invalid due to its length, or sequence
|
||||||
|
// of characters. See validateLabelKey for more details.
|
||||||
//
|
//
|
||||||
// The empty string is a valid value in the input values set.
|
// The empty string is a valid value in the input values set.
|
||||||
func NewRequirement(key string, op Operator, vals util.StringSet) (*Requirement, error) {
|
func NewRequirement(key string, op Operator, vals util.StringSet) (*Requirement, error) {
|
||||||
|
if err := validateLabelKey(key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
switch op {
|
switch op {
|
||||||
case In, NotIn:
|
case In, NotIn:
|
||||||
if len(vals) == 0 {
|
if len(vals) == 0 {
|
||||||
@ -281,16 +297,142 @@ func (lsel *LabelSelector) String() (string, error) {
|
|||||||
return strings.Join(reqs, ","), nil
|
return strings.Join(reqs, ","), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Parse takes a string representing a selector and returns
|
// Parse takes a string representing a selector and returns a selector
|
||||||
// a selector, or an error. A well-formed input string follows
|
// object, or an error. This parsing function differs from ParseSelector
|
||||||
// the syntax of that which is returned by LabelSelector.String
|
// as they parse different selectors with different syntaxes.
|
||||||
// and therefore is largely controlled by that which is returned
|
// The input will cause an error if it does not follow this form:
|
||||||
// by Requirement.String. The returned selector object's type
|
//
|
||||||
// should be an interface implemented by LabelSelector. Note that
|
// <selector-syntax> ::= <requirement> | <requirement> "," <selector-syntax>
|
||||||
// this parsing function is different than ParseSelector since
|
// <requirement> ::= KEY <set-restriction>
|
||||||
// they parse different selectors with different syntaxes.
|
// <set-restriction> ::= "" | <inclusion-exclusion> <value-set>
|
||||||
// See comments above for LabelSelector and Requirement struct
|
// <inclusion-exclusion> ::= " in " | " not in "
|
||||||
// definition for more details.
|
// <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 ')'
|
||||||
|
// [^, )]*
|
||||||
|
//
|
||||||
|
// Example of valid syntax:
|
||||||
|
// "x in (foo,,baz),y,z not in ()"
|
||||||
|
//
|
||||||
|
// Note:
|
||||||
|
// (1) Inclusion - " in " - denotes that the KEY is equal to any of the
|
||||||
|
// VALUEs in its requirement
|
||||||
|
// (2) Exclusion - " not in " - denotes that the KEY is not equal to any
|
||||||
|
// of the VALUEs in its requirement
|
||||||
|
// (3) The empty string is a valid VALUE
|
||||||
|
// (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 inPre = "in ("
|
||||||
|
const notInPre = "not in ("
|
||||||
|
const pos = "position %d:%s"
|
||||||
|
|
||||||
|
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 ' ':
|
||||||
|
return nil, fmt.Errorf("white space not allowed before key. "+pos, i, selector)
|
||||||
|
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 ' ':
|
||||||
|
state = waitOp
|
||||||
|
key = selector[strStart:i]
|
||||||
|
}
|
||||||
|
case waitOp:
|
||||||
|
if len(selector)-i >= len(inPre) && selector[i:len(inPre)+i] == inPre {
|
||||||
|
op = In
|
||||||
|
i += len(inPre) - 1
|
||||||
|
} else if len(selector)-i >= len(notInPre) && selector[i:len(notInPre)+i] == notInPre {
|
||||||
|
op = NotIn
|
||||||
|
i += len(notInPre) - 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: unify with validation.validateLabels
|
||||||
|
func validateLabelKey(k string) error {
|
||||||
|
if !util.IsDNS952Label(k) {
|
||||||
|
return errors.NewFieldNotSupported("key", k)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func try(selectorPiece, op string) (lhs, rhs string, ok bool) {
|
func try(selectorPiece, op string) (lhs, rhs string, ok bool) {
|
||||||
pieces := strings.Split(selectorPiece, op)
|
pieces := strings.Split(selectorPiece, op)
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package labels
|
package labels
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
@ -212,6 +213,8 @@ func TestRequirementConstructor(t *testing.T) {
|
|||||||
{"x", In, util.NewStringSet("foo"), true},
|
{"x", In, util.NewStringSet("foo"), true},
|
||||||
{"x", NotIn, util.NewStringSet("foo"), true},
|
{"x", NotIn, util.NewStringSet("foo"), true},
|
||||||
{"x", Exists, nil, true},
|
{"x", Exists, nil, true},
|
||||||
|
{"abcdefghijklmnopqrstuvwxy", Exists, nil, false}, //breaks DNS952 rule that len(key) < 25
|
||||||
|
{"1foo", In, util.NewStringSet("bar"), false}, //breaks DNS952 rule that keys start with [a-z]
|
||||||
}
|
}
|
||||||
for _, rc := range requirementConstructorTests {
|
for _, rc := range requirementConstructorTests {
|
||||||
if _, err := NewRequirement(rc.Key, rc.Op, rc.Vals); err == nil && !rc.Success {
|
if _, err := NewRequirement(rc.Key, rc.Op, rc.Vals); err == nil && !rc.Success {
|
||||||
@ -289,6 +292,67 @@ func TestRequirementLabelSelectorMatching(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetSelectorParser(t *testing.T) {
|
||||||
|
setSelectorParserTests := []struct {
|
||||||
|
In string
|
||||||
|
Out SetBasedSelector
|
||||||
|
Match bool
|
||||||
|
Valid bool
|
||||||
|
}{
|
||||||
|
{"", &LabelSelector{Requirements: nil}, true, true},
|
||||||
|
{"x", &LabelSelector{Requirements: []Requirement{
|
||||||
|
getRequirement("x", Exists, nil, t),
|
||||||
|
}}, true, true},
|
||||||
|
{"foo in (abc)", &LabelSelector{Requirements: []Requirement{
|
||||||
|
getRequirement("foo", In, util.NewStringSet("abc"), t),
|
||||||
|
}}, true, true},
|
||||||
|
{"x not in (abc)", &LabelSelector{Requirements: []Requirement{
|
||||||
|
getRequirement("x", NotIn, util.NewStringSet("abc"), t),
|
||||||
|
}}, true, true},
|
||||||
|
{"x not in (abc,def)", &LabelSelector{Requirements: []Requirement{
|
||||||
|
getRequirement("x", NotIn, util.NewStringSet("abc", "def"), t),
|
||||||
|
}}, true, true},
|
||||||
|
{"x in (abc,def)", &LabelSelector{Requirements: []Requirement{
|
||||||
|
getRequirement("x", In, util.NewStringSet("abc", "def"), t),
|
||||||
|
}}, true, true},
|
||||||
|
{"x in (abc,)", &LabelSelector{Requirements: []Requirement{
|
||||||
|
getRequirement("x", In, util.NewStringSet("abc", ""), t),
|
||||||
|
}}, true, true},
|
||||||
|
{"x in ()", &LabelSelector{Requirements: []Requirement{
|
||||||
|
getRequirement("x", In, 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),
|
||||||
|
}}, true, true},
|
||||||
|
{"x,y in (a)", &LabelSelector{Requirements: []Requirement{
|
||||||
|
getRequirement("y", In, util.NewStringSet("a"), t),
|
||||||
|
getRequirement("x", Exists, nil, t),
|
||||||
|
}}, false, true},
|
||||||
|
{"x,,y", nil, true, 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},
|
||||||
|
{"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},
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getRequirement(key string, op Operator, vals util.StringSet, t *testing.T) Requirement {
|
func getRequirement(key string, op Operator, vals util.StringSet, t *testing.T) Requirement {
|
||||||
req, err := NewRequirement(key, op, vals)
|
req, err := NewRequirement(key, op, vals)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user