mirror of
https://github.com/rancher/steve.git
synced 2025-06-30 00:32:07 +00:00
* Add more fields to index when sql-caching is on. * Restore the gvkKey helper, add event fields. The UI team wasn't sure whether the event fields should go in the empty-string group or in 'events.k8s.io', so let's go with both until/unless specified otherwise. * More fixes to the fields to index: - Remove the erroneously added management.cattle.io.nodes fields - Use the builtin Event class, not events.k8s.io (by looking at the dashboard client code) * Start on the virtual-field work. * Map `Event.type` to `Event._type` for indexing. * Add a unit test for field replacement for Event.type * Add label processing. * Don't test for transformation of event objects in the common module. * Parse metadata.label queries differently. * Improve a variable name that turned out to not be temporary. * No need to specifically cache certain labels, as all are now cached. * Add a test to verify simple label (m.labels.foo=blah) queries work. * 'addLabelFields' never returns an error. * Delete superseded function. * Was calling 'addLabelFields' one time too many. * Start using k8s ParseToRequirements * Pull in the k8s parser. * Successfully test for quotation marks. * Add quoted strings to the lexer. * Move to a forked k8s label lexer to include non-label tests. * Improve and test the way quoted strings in the query are detected. * Reinstate the original Apache license in the derived code. Following clause 4.3 of the Apache license: "You must cause any modified files to carry prominent notices stating that You changed the files..." * Ignore case for operators. * Test IN multiple-target-values * Test the not-in operator. * Ignore case for operators. SQL is case-insensitive on field names and values, so this just adds consistency. * Added tests for parsing EXISTS and NOT-EXISTS queries. * Parse less-than and greater-than ops * Lasso's `CacheFor` now takes a `watchable` argument. * Support 'gt' and 'lt' as synonyms for '<' and '>'. I see both types of operators being bandied about -- it's easy to support the aliases. * typo fix * Have the filter parser allow exist tests only on labels. Also reduce the case where there's no namespace function. * Specify hard-wired fields to index alphabetically. * Remove unused variable. * Parser: 'metadata.labels[FIELD]' is valid * Pull in new gvk fields from main (and keep in alpha order). * Fixed a couple of drops done during the last rebase. * Add a reminder to keep the entries in alpha order. * Test TransformLabels * Remove TransformLabels * Remove unused/unneeded code. * Describe diffs between our label-selector parser and upstream's. * Use the merged lasso 46333 work. * Drop unused field. * Tighten up the code. * Specify which commit the label selector parser is based on. * Allow both single-quoted and double-quoted value matching, doc difference. * More review-driven changes: - Stricter processing of m.l.name keys: Require ending close-bracket for a start-bracket - Comment fix - Moving sql processing from lasso to steve: some changes missed in rebase * Drop support for double-quotes for string values. For now on only single-quotes (or none where possible) are allowed. * Renaming and dropping an init block. * Quoted strings are dropped from the filter queries In particular, label values have a specific syntax: they must start and end with a letter, and their innards may contain only alnums '.', '-' and '_'. So there's no need for quoting. And that means now that `=` and `==` do exact matches, and the `~` operator does a partial match. `!=` and `!~` negate -- note that `!~` is a stricter operation than `!=`, in that given a set of possible string values, `!=` will accept more of them than `!~`. Maybe I shouldn't have gone here, but these operators reminded me of learning about `nicht durfen` and `nicht sollen` in German, or something like that. * Move a constant definition to the module level. * Remove commented-out code. * Remove unused func and adjacent redundant comment.
420 lines
12 KiB
Go
420 lines
12 KiB
Go
/*
|
|
Copyright 2014 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
/*
|
|
This file is derived from
|
|
https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector_test.go
|
|
*/
|
|
|
|
package queryparser
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/rancher/steve/pkg/stores/sqlpartition/selection"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
)
|
|
|
|
var (
|
|
ignoreDetail = cmpopts.IgnoreFields(field.Error{}, "Detail")
|
|
)
|
|
|
|
func TestSelectorParse(t *testing.T) {
|
|
testGoodStrings := []string{
|
|
"x=a,y=b,z=c",
|
|
"",
|
|
"x!=a,y=b",
|
|
"close ~ value",
|
|
"notclose !~ value",
|
|
"x>1",
|
|
"x>1,z<5",
|
|
"x gt 1,z lt 5",
|
|
`y == def`,
|
|
"metadata.labels.im-here",
|
|
"!metadata.labels.im-not-here",
|
|
"metadata.labels[im.here]",
|
|
"!metadata.labels[im.not.here]",
|
|
"metadata.labels[k8s.io/meta-stuff] ~ has-dashes_underscores.dots.only",
|
|
}
|
|
testBadStrings := []string{
|
|
"!no-label-absence-test",
|
|
"no-label-presence-test",
|
|
"x=a||y=b",
|
|
"x==a==b",
|
|
"!x=a",
|
|
"x<a",
|
|
"x=",
|
|
"x= ",
|
|
"x=,z= ",
|
|
"x= ,z= ",
|
|
"x ~",
|
|
"x !~",
|
|
"~ val",
|
|
"!~ val",
|
|
"= val",
|
|
"== val",
|
|
"metadata.labels-im.here",
|
|
"metadata.labels[missing/close-bracket",
|
|
"!metadata.labels(im.not.here)",
|
|
`x="no double quotes allowed"`,
|
|
`x='no single quotes allowed'`,
|
|
}
|
|
for _, test := range testGoodStrings {
|
|
_, err := Parse(test)
|
|
if err != nil {
|
|
t.Errorf("%v: error %v (%#v)\n", test, err, err)
|
|
}
|
|
}
|
|
for _, test := range testBadStrings {
|
|
_, err := Parse(test)
|
|
if err == nil {
|
|
t.Errorf("%v: did not get expected error\n", test)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLexer(t *testing.T) {
|
|
testcases := []struct {
|
|
s string
|
|
t Token
|
|
}{
|
|
{"", EndOfStringToken},
|
|
{",", CommaToken},
|
|
{"notin", NotInToken},
|
|
{"in", InToken},
|
|
{"=", EqualsToken},
|
|
{"==", DoubleEqualsToken},
|
|
{">", GreaterThanToken},
|
|
{"<", LessThanToken},
|
|
//Note that Lex returns the longest valid token found
|
|
{"!", DoesNotExistToken},
|
|
{"!=", NotEqualsToken},
|
|
{"(", OpenParToken},
|
|
{")", ClosedParToken},
|
|
{`'sq string''`, ErrorToken},
|
|
{`"dq string"`, ErrorToken},
|
|
{"~", PartialEqualsToken},
|
|
{"!~", NotPartialEqualsToken},
|
|
{"||", ErrorToken},
|
|
}
|
|
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 != ErrorToken && 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{IdentifierToken, InToken, OpenParToken, IdentifierToken, ClosedParToken}},
|
|
{"key notin ( value )", []Token{IdentifierToken, NotInToken, OpenParToken, IdentifierToken, ClosedParToken}},
|
|
{"key in ( value1, value2 )", []Token{IdentifierToken, InToken, OpenParToken, IdentifierToken, CommaToken, IdentifierToken, ClosedParToken}},
|
|
{"key", []Token{IdentifierToken}},
|
|
{"!key", []Token{DoesNotExistToken, IdentifierToken}},
|
|
{"()", []Token{OpenParToken, ClosedParToken}},
|
|
{"x in (),y", []Token{IdentifierToken, InToken, OpenParToken, ClosedParToken, CommaToken, IdentifierToken}},
|
|
{"== != (), = notin", []Token{DoubleEqualsToken, NotEqualsToken, OpenParToken, ClosedParToken, CommaToken, EqualsToken, NotInToken}},
|
|
{"key>2", []Token{IdentifierToken, GreaterThanToken, IdentifierToken}},
|
|
{"key<1", []Token{IdentifierToken, LessThanToken, IdentifierToken}},
|
|
{"key gt 3", []Token{IdentifierToken, IdentifierToken, IdentifierToken}},
|
|
{"key lt 4", []Token{IdentifierToken, IdentifierToken, IdentifierToken}},
|
|
{"key=value", []Token{IdentifierToken, EqualsToken, IdentifierToken}},
|
|
{"key == value", []Token{IdentifierToken, DoubleEqualsToken, IdentifierToken}},
|
|
{"key ~ value", []Token{IdentifierToken, PartialEqualsToken, IdentifierToken}},
|
|
{"key~ value", []Token{IdentifierToken, PartialEqualsToken, IdentifierToken}},
|
|
{"key ~value", []Token{IdentifierToken, PartialEqualsToken, IdentifierToken}},
|
|
{"key~value", []Token{IdentifierToken, PartialEqualsToken, IdentifierToken}},
|
|
{"key !~ value", []Token{IdentifierToken, NotPartialEqualsToken, IdentifierToken}},
|
|
{"key!~ value", []Token{IdentifierToken, NotPartialEqualsToken, IdentifierToken}},
|
|
{"key !~value", []Token{IdentifierToken, NotPartialEqualsToken, IdentifierToken}},
|
|
{"key!~value", []Token{IdentifierToken, NotPartialEqualsToken, IdentifierToken}},
|
|
}
|
|
for _, v := range testcases {
|
|
var tokens []Token
|
|
l := &Lexer{s: v.s, pos: 0}
|
|
for {
|
|
token, _ := l.Lex()
|
|
if token == EndOfStringToken {
|
|
break
|
|
}
|
|
tokens = append(tokens, token)
|
|
}
|
|
if len(tokens) != len(v.t) {
|
|
t.Errorf("Bad number of tokens for '%s': got %d, wanted %d (got %v)", v.s, len(tokens), len(v.t), tokens)
|
|
}
|
|
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 '%v' it should be '%v'", v.s, tokens[i], v.t[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
func TestParserLookahead(t *testing.T) {
|
|
testcases := []struct {
|
|
s string
|
|
t []Token
|
|
}{
|
|
{"key in ( value )", []Token{IdentifierToken, InToken, OpenParToken, IdentifierToken, ClosedParToken, EndOfStringToken}},
|
|
{"key notin ( value )", []Token{IdentifierToken, NotInToken, OpenParToken, IdentifierToken, ClosedParToken, EndOfStringToken}},
|
|
{"key in ( value1, value2 )", []Token{IdentifierToken, InToken, OpenParToken, IdentifierToken, CommaToken, IdentifierToken, ClosedParToken, EndOfStringToken}},
|
|
{"key", []Token{IdentifierToken, EndOfStringToken}},
|
|
{"!key", []Token{DoesNotExistToken, IdentifierToken, EndOfStringToken}},
|
|
{"()", []Token{OpenParToken, ClosedParToken, EndOfStringToken}},
|
|
{"", []Token{EndOfStringToken}},
|
|
{"x in (),y", []Token{IdentifierToken, InToken, OpenParToken, ClosedParToken, CommaToken, IdentifierToken, EndOfStringToken}},
|
|
{"== != (), = notin", []Token{DoubleEqualsToken, NotEqualsToken, OpenParToken, ClosedParToken, CommaToken, EqualsToken, NotInToken, EndOfStringToken}},
|
|
{"key>2", []Token{IdentifierToken, GreaterThanToken, IdentifierToken, EndOfStringToken}},
|
|
{"key<1", []Token{IdentifierToken, LessThanToken, IdentifierToken, EndOfStringToken}},
|
|
{"key gt 3", []Token{IdentifierToken, GreaterThanToken, IdentifierToken, EndOfStringToken}},
|
|
{"key lt 4", []Token{IdentifierToken, LessThanToken, IdentifierToken, EndOfStringToken}},
|
|
{`key = multi-word-string`, []Token{IdentifierToken, EqualsToken, QuotedStringToken, EndOfStringToken}},
|
|
}
|
|
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 for test %s, found %d", len(v.t), v.s, len(p.scannedItems))
|
|
}
|
|
for {
|
|
token, lit := p.lookahead(KeyAndOperator)
|
|
|
|
token2, lit2 := p.consume(KeyAndOperator)
|
|
if token == EndOfStringToken {
|
|
break
|
|
}
|
|
if token != token2 || lit != lit2 {
|
|
t.Errorf("Bad values")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseOperator(t *testing.T) {
|
|
testcases := []struct {
|
|
token string
|
|
expectedError error
|
|
}{
|
|
{"in", nil},
|
|
{"=", nil},
|
|
{"==", nil},
|
|
{"~", nil},
|
|
{">", nil},
|
|
{"<", nil},
|
|
{"lt", nil},
|
|
{"gt", nil},
|
|
{"notin", nil},
|
|
{"!=", nil},
|
|
{"!~", nil},
|
|
{"!", fmt.Errorf("found '%s', expected: %v", selection.DoesNotExist, strings.Join(binaryOperators, ", "))},
|
|
{"exists", fmt.Errorf("found '%s', expected: %v", selection.Exists, strings.Join(binaryOperators, ", "))},
|
|
{"(", fmt.Errorf("found '%s', expected: %v", "(", strings.Join(binaryOperators, ", "))},
|
|
}
|
|
for _, testcase := range testcases {
|
|
p := &Parser{l: &Lexer{s: testcase.token, pos: 0}, position: 0}
|
|
p.scan()
|
|
|
|
_, err := p.parseOperator()
|
|
if ok := reflect.DeepEqual(testcase.expectedError, err); !ok {
|
|
t.Errorf("\nexpect err [%v], \nactual err [%v]", testcase.expectedError, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Some error fields are commented out here because this fork no longer
|
|
// enforces k8s label expression lexical and length restrictions
|
|
func TestRequirementConstructor(t *testing.T) {
|
|
requirementConstructorTests := []struct {
|
|
Key string
|
|
Op selection.Operator
|
|
Vals sets.String
|
|
WantErr field.ErrorList
|
|
}{
|
|
{
|
|
Key: "x1",
|
|
Op: selection.In,
|
|
WantErr: field.ErrorList{
|
|
&field.Error{
|
|
Type: field.ErrorTypeInvalid,
|
|
Field: "values",
|
|
BadValue: []string{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Key: "x2",
|
|
Op: selection.NotIn,
|
|
Vals: sets.NewString(),
|
|
WantErr: field.ErrorList{
|
|
&field.Error{
|
|
Type: field.ErrorTypeInvalid,
|
|
Field: "values",
|
|
BadValue: []string{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Key: "x3",
|
|
Op: selection.In,
|
|
Vals: sets.NewString("foo"),
|
|
},
|
|
{
|
|
Key: "x4",
|
|
Op: selection.NotIn,
|
|
Vals: sets.NewString("foo"),
|
|
},
|
|
{
|
|
Key: "x5",
|
|
Op: selection.Equals,
|
|
Vals: sets.NewString("foo", "bar"),
|
|
WantErr: field.ErrorList{
|
|
&field.Error{
|
|
Type: field.ErrorTypeInvalid,
|
|
Field: "values",
|
|
BadValue: []string{"bar", "foo"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Key: "x6",
|
|
Op: selection.Exists,
|
|
},
|
|
{
|
|
Key: "x7",
|
|
Op: selection.DoesNotExist,
|
|
},
|
|
{
|
|
Key: "x8",
|
|
Op: selection.Exists,
|
|
Vals: sets.NewString("foo"),
|
|
WantErr: field.ErrorList{
|
|
&field.Error{
|
|
Type: field.ErrorTypeInvalid,
|
|
Field: "values",
|
|
BadValue: []string{"foo"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Key: "x9",
|
|
Op: selection.In,
|
|
Vals: sets.NewString("bar"),
|
|
},
|
|
{
|
|
Key: "x10",
|
|
Op: selection.In,
|
|
Vals: sets.NewString("bar"),
|
|
},
|
|
{
|
|
Key: "x11",
|
|
Op: selection.GreaterThan,
|
|
Vals: sets.NewString("1"),
|
|
},
|
|
{
|
|
Key: "x12",
|
|
Op: selection.LessThan,
|
|
Vals: sets.NewString("6"),
|
|
},
|
|
{
|
|
Key: "x13",
|
|
Op: selection.GreaterThan,
|
|
WantErr: field.ErrorList{
|
|
&field.Error{
|
|
Type: field.ErrorTypeInvalid,
|
|
Field: "values",
|
|
BadValue: []string{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Key: "x14",
|
|
Op: selection.GreaterThan,
|
|
Vals: sets.NewString("bar"),
|
|
WantErr: field.ErrorList{
|
|
&field.Error{
|
|
Type: field.ErrorTypeInvalid,
|
|
Field: "values[0]",
|
|
BadValue: "bar",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Key: "x15",
|
|
Op: selection.LessThan,
|
|
Vals: sets.NewString("bar"),
|
|
WantErr: field.ErrorList{
|
|
&field.Error{
|
|
Type: field.ErrorTypeInvalid,
|
|
Field: "values[0]",
|
|
BadValue: "bar",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Key: strings.Repeat("a", 254), //breaks DNS rule that len(key) <= 253
|
|
Op: selection.Exists,
|
|
},
|
|
{
|
|
Key: "x16",
|
|
Op: selection.Equals,
|
|
Vals: sets.NewString(strings.Repeat("a", 254)),
|
|
},
|
|
{
|
|
Key: "x17",
|
|
Op: selection.Equals,
|
|
Vals: sets.NewString("a b"),
|
|
},
|
|
{
|
|
Key: "x18",
|
|
Op: "unsupportedOp",
|
|
WantErr: field.ErrorList{
|
|
&field.Error{
|
|
Type: field.ErrorTypeNotSupported,
|
|
Field: "operator",
|
|
BadValue: selection.Operator("unsupportedOp"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, rc := range requirementConstructorTests {
|
|
_, err := NewRequirement(rc.Key, rc.Op, rc.Vals.List())
|
|
if diff := cmp.Diff(rc.WantErr.ToAggregate(), err, ignoreDetail); diff != "" {
|
|
t.Errorf("NewRequirement test %v returned unexpected error (-want,+got):\n%s", rc.Key, diff)
|
|
}
|
|
}
|
|
}
|