
With this PR, we add the syntax to use for filtration of items over the containerd API. This package defines a syntax and parser that can be used across types and use cases in a uniform manner. The syntax is fairly familiar, if you've used container ecosystem projects. At the core, we base it on the concept of protobuf field paths, augmenting with the ability to quote portions of the field path to match arbitrary labels. These "selectors" come in the following syntax: ``` <fieldpath>[<operator><value>] ``` A basic example is as follows: ``` name=foo ``` This would match all objects that have a field `name` with the value `foo`. If we only want to test if the field is present, we can omit the operator. This is most useful for matching labels in containerd. The following will match objects that has the field labels and have the label "foo" defined: ``` labels.foo ``` We also allow for quoting of parts of the field path to allow matching of arbitrary items: ``` labels."very complex label"==something ``` We also define `!=` and `~=` as operators. The `!=` operator will match all objects that don't match the value for a field and `~=` will compile the target value as a regular expression and match the field value against that. Selectors can be combined using a comma, such that the resulting selector will require all selectors are matched for the object to match. The following example will match objects that are named `foo` and have the label `bar`: ``` name==foo,labels.bar ``` This filter syntax will be used across all APIs that allow listing of objects and for filtering which event a cleint see. By using a common syntax, we hope to keep API access uniform. For the most part, this takes inspiration from docker, swarm and k8s, but has the limitation that it only allows selection of an inner product. We may expand to operators that implement `or`, `in` or `notin`, but it is not clear that this is useful at this level of the stack. Signed-off-by: Stephen J Day <stephen.day@docker.com>
240 lines
4.5 KiB
Go
240 lines
4.5 KiB
Go
package filters
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
/*
|
|
Parse the strings into a filter that may be used with an adaptor.
|
|
|
|
The filter is made up of zero or more selectors.
|
|
|
|
The format is a comma separated list of expressions, in the form of
|
|
`<fieldpath><op><value>`, known as selectors. All selectors must match the
|
|
target object for the filter to be true.
|
|
|
|
We define the operators "==" for equality, "!=" for not equal and "~=" for a
|
|
regular expression. If the operator and value are not present, the matcher will
|
|
test for the presence of a value, as defined by the target object.
|
|
|
|
The formal grammar is as follows:
|
|
|
|
selectors := selector ("," selector)*
|
|
selector := fieldpath (operator value)
|
|
fieldpath := field ('.' field)*
|
|
field := quoted | [A-Za-z] [A-Za-z0-9_]+
|
|
operator := "==" | "!=" | "~="
|
|
value := quoted | [^\s,]+
|
|
quoted := <go string syntax>
|
|
|
|
*/
|
|
func Parse(s string) (Filter, error) {
|
|
// special case empty to match all
|
|
if s == "" {
|
|
return Always, nil
|
|
}
|
|
|
|
p := parser{input: s}
|
|
return p.parse()
|
|
}
|
|
|
|
type parser struct {
|
|
input string
|
|
scanner scanner
|
|
}
|
|
|
|
func (p *parser) parse() (Filter, error) {
|
|
p.scanner.init(p.input)
|
|
|
|
ss, err := p.selectors()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "filters")
|
|
}
|
|
|
|
return ss, nil
|
|
}
|
|
|
|
func (p *parser) selectors() (Filter, error) {
|
|
s, err := p.selector()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ss := All{s}
|
|
|
|
loop:
|
|
for {
|
|
tok := p.scanner.peek()
|
|
switch tok {
|
|
case ',':
|
|
pos, tok, _ := p.scanner.scan()
|
|
if tok != tokenSelectorSeparator {
|
|
return nil, p.mkerr(pos, "expected a separator")
|
|
}
|
|
|
|
s, err := p.selector()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ss = append(ss, s)
|
|
case tokenEOF:
|
|
break loop
|
|
default:
|
|
panic("unconsumed input")
|
|
}
|
|
}
|
|
|
|
return ss, nil
|
|
}
|
|
|
|
func (p *parser) selector() (selector, error) {
|
|
fieldpath, err := p.fieldpath()
|
|
if err != nil {
|
|
return selector{}, err
|
|
}
|
|
|
|
switch p.scanner.peek() {
|
|
case tokenSelectorSeparator, tokenEOF:
|
|
return selector{
|
|
fieldpath: fieldpath,
|
|
operator: operatorPresent,
|
|
}, nil
|
|
}
|
|
|
|
op, err := p.operator()
|
|
if err != nil {
|
|
return selector{}, err
|
|
}
|
|
|
|
value, err := p.value()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return selector{}, io.ErrUnexpectedEOF
|
|
}
|
|
return selector{}, err
|
|
}
|
|
|
|
return selector{
|
|
fieldpath: fieldpath,
|
|
value: value,
|
|
operator: op,
|
|
}, nil
|
|
}
|
|
|
|
func (p *parser) fieldpath() ([]string, error) {
|
|
f, err := p.field()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fs := []string{f}
|
|
loop:
|
|
for {
|
|
tok := p.scanner.peek() // lookahead to consume field separtor
|
|
|
|
switch tok {
|
|
case '.':
|
|
pos, tok, _ := p.scanner.scan() // consume separator
|
|
if tok != tokenFieldSeparator {
|
|
return nil, p.mkerr(pos, "expected a field separator (`.`)")
|
|
}
|
|
|
|
f, err := p.field()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fs = append(fs, f)
|
|
default:
|
|
// let the layer above handle the other bad cases.
|
|
break loop
|
|
}
|
|
}
|
|
|
|
return fs, nil
|
|
}
|
|
|
|
func (p *parser) field() (string, error) {
|
|
pos, tok, s := p.scanner.scan()
|
|
switch tok {
|
|
case tokenField:
|
|
return s, nil
|
|
case tokenQuoted:
|
|
return p.unquote(pos, s)
|
|
}
|
|
|
|
return "", p.mkerr(pos, "expected field or quoted")
|
|
}
|
|
|
|
func (p *parser) operator() (operator, error) {
|
|
pos, tok, s := p.scanner.scan()
|
|
switch tok {
|
|
case tokenOperator:
|
|
switch s {
|
|
case "==":
|
|
return operatorEqual, nil
|
|
case "!=":
|
|
return operatorNotEqual, nil
|
|
case "~=":
|
|
return operatorMatches, nil
|
|
default:
|
|
return 0, p.mkerr(pos, "unsupported operator %q", s)
|
|
}
|
|
}
|
|
|
|
return 0, p.mkerr(pos, `expected an operator ("=="|"!="|"~=")`)
|
|
}
|
|
|
|
func (p *parser) value() (string, error) {
|
|
pos, tok, s := p.scanner.scan()
|
|
|
|
switch tok {
|
|
case tokenValue, tokenField:
|
|
return s, nil
|
|
case tokenQuoted:
|
|
return p.unquote(pos, s)
|
|
}
|
|
|
|
return "", p.mkerr(pos, "expected value or quoted")
|
|
}
|
|
|
|
func (p *parser) unquote(pos int, s string) (string, error) {
|
|
uq, err := strconv.Unquote(s)
|
|
if err != nil {
|
|
return "", p.mkerr(pos, "unquoting failed: %v", err)
|
|
}
|
|
|
|
return uq, nil
|
|
}
|
|
|
|
type parseError struct {
|
|
input string
|
|
pos int
|
|
msg string
|
|
}
|
|
|
|
func (pe parseError) Error() string {
|
|
if pe.pos < len(pe.input) {
|
|
before := pe.input[:pe.pos]
|
|
location := pe.input[pe.pos : pe.pos+1] // need to handle end
|
|
after := pe.input[pe.pos+1:]
|
|
|
|
return fmt.Sprintf("[%s >|%s|< %s]: %v", before, location, after, pe.msg)
|
|
}
|
|
|
|
return fmt.Sprintf("[%s]: %v", pe.input, pe.msg)
|
|
}
|
|
|
|
func (p *parser) mkerr(pos int, format string, args ...interface{}) error {
|
|
return errors.Wrap(parseError{
|
|
input: p.input,
|
|
pos: pos,
|
|
msg: fmt.Sprintf(format, args...),
|
|
}, "parse error")
|
|
}
|