mirror of
https://github.com/rancher/steve.git
synced 2025-07-17 16:31:30 +00:00
Generate field names with brackets when needed. (#477)
* Generate field names with brackets when needed. * Stop hard-wiring complex selectors as `["field1", "field2[sub-field3]"]` and instead represent them as a more consistent `["field1", "field2", "sub-field3"]` * Convert all filter strings ending with square brackets to an array. Stop special-casing 'metadata.labels[X]' and handle any query string that ends with '[...]'. * Stop checking for pre-bracketed terms in constant field-accessor arrays. In this commit we stop converting string arrays like `["metadata", "labels[k8s.io/deepcode]"]` into the database field `"metadata.labels[k8s.io/deepcode]"` and instead will do a naive `join` to give `metadata[labels[k8s.io/deepcode]]`. The solution is to never express the above terms in separate fields, like `["metadata", "labels", "k8s.io/deepcode"]`. This also better reflects the stucture of the referenced object. * gofmt changes * Simplify comment about 'smartJoin'.
This commit is contained in:
parent
527d44a3a7
commit
3b45729415
@ -742,9 +742,28 @@ func formatMatchTargetWithFormatter(match string, format string) string {
|
|||||||
return fmt.Sprintf(format, match)
|
return fmt.Sprintf(format, match)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// There are two kinds of string arrays to turn into a string, based on the last value in the array
|
||||||
|
// simple: ["a", "b", "conformsToIdentifier"] => "a.b.conformsToIdentifier"
|
||||||
|
// complex: ["a", "b", "foo.io/stuff"] => "a.b[foo.io/stuff]"
|
||||||
|
|
||||||
|
func smartJoin(s []string) string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(s) == 1 {
|
||||||
|
return s[0]
|
||||||
|
}
|
||||||
|
lastBit := s[len(s)-1]
|
||||||
|
simpleName := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||||
|
if simpleName.MatchString(lastBit) {
|
||||||
|
return strings.Join(s, ".")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s[%s]", strings.Join(s[0:len(s)-1], "."), lastBit)
|
||||||
|
}
|
||||||
|
|
||||||
// toColumnName returns the column name corresponding to a field expressed as string slice
|
// toColumnName returns the column name corresponding to a field expressed as string slice
|
||||||
func toColumnName(s []string) string {
|
func toColumnName(s []string) string {
|
||||||
return db.Sanitize(strings.Join(s, "."))
|
return db.Sanitize(smartJoin(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
// getField extracts the value of a field expressed as a string path from an unstructured object
|
// getField extracts the value of a field expressed as a string path from an unstructured object
|
||||||
|
@ -262,6 +262,7 @@ func TestListByOptions(t *testing.T) {
|
|||||||
expectedCountStmtArgs []any
|
expectedCountStmtArgs []any
|
||||||
expectedStmt string
|
expectedStmt string
|
||||||
expectedStmtArgs []any
|
expectedStmtArgs []any
|
||||||
|
extraIndexedFields []string
|
||||||
expectedList *unstructured.UnstructuredList
|
expectedList *unstructured.UnstructuredList
|
||||||
returnList []any
|
returnList []any
|
||||||
expectedContToken string
|
expectedContToken string
|
||||||
@ -688,6 +689,27 @@ func TestListByOptions(t *testing.T) {
|
|||||||
expectedErr: nil,
|
expectedErr: nil,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "ListByOptions sorting on two complex fields should sort on the first field in ascending order first and then sort on the second field in ascending order in prepared sql.Stmt",
|
||||||
|
listOptions: ListOptions{
|
||||||
|
Sort: Sort{
|
||||||
|
Fields: [][]string{{"metadata", "fields", "3"}, {"metadata", "labels", "stub.io/candy"}},
|
||||||
|
Orders: []SortOrder{ASC, ASC},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraIndexedFields: []string{"metadata.fields[3]", "metadata.labels[stub.io/candy]"},
|
||||||
|
partitions: []partition.Partition{},
|
||||||
|
ns: "",
|
||||||
|
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||||
|
JOIN "something_fields" f ON o.key = f.key
|
||||||
|
WHERE
|
||||||
|
(FALSE)
|
||||||
|
ORDER BY f."metadata.fields[3]" ASC, f."metadata.labels[stub.io/candy]" ASC`,
|
||||||
|
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||||
|
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||||
|
expectedContToken: "",
|
||||||
|
expectedErr: nil,
|
||||||
|
})
|
||||||
tests = append(tests, testCase{
|
tests = append(tests, testCase{
|
||||||
description: "ListByOptions sorting on two fields should sort on the first field in ascending order first and then sort on the second field in ascending order in prepared sql.Stmt",
|
description: "ListByOptions sorting on two fields should sort on the first field in ascending order first and then sort on the second field in ascending order in prepared sql.Stmt",
|
||||||
listOptions: ListOptions{
|
listOptions: ListOptions{
|
||||||
@ -909,8 +931,8 @@ func TestListByOptions(t *testing.T) {
|
|||||||
Indexer: i,
|
Indexer: i,
|
||||||
indexedFields: []string{"metadata.somefield", "status.someotherfield"},
|
indexedFields: []string{"metadata.somefield", "status.someotherfield"},
|
||||||
}
|
}
|
||||||
if test.description == "ListByOptions with multiple OrFilters set should select where all OrFilters contain one filter that is true in prepared sql.Stmt" {
|
if len(test.extraIndexedFields) > 0 {
|
||||||
fmt.Printf("stop here")
|
lii.indexedFields = append(lii.indexedFields, test.extraIndexedFields...)
|
||||||
}
|
}
|
||||||
queryInfo, err := lii.constructQuery(test.listOptions, test.partitions, test.ns, "something")
|
queryInfo, err := lii.constructQuery(test.listOptions, test.partitions, test.ns, "something")
|
||||||
if test.expectedErr != nil {
|
if test.expectedErr != nil {
|
||||||
@ -1536,3 +1558,65 @@ func TestConstructQuery(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSmartJoin(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
description string
|
||||||
|
fieldArray []string
|
||||||
|
expectedFieldName string
|
||||||
|
}
|
||||||
|
|
||||||
|
var tests []testCase
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "single-letter names should be dotted",
|
||||||
|
fieldArray: []string{"metadata", "labels", "a"},
|
||||||
|
expectedFieldName: "metadata.labels.a",
|
||||||
|
})
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "underscore should be dotted",
|
||||||
|
fieldArray: []string{"metadata", "labels", "_"},
|
||||||
|
expectedFieldName: "metadata.labels._",
|
||||||
|
})
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "simple names should be dotted",
|
||||||
|
fieldArray: []string{"metadata", "labels", "queryField2"},
|
||||||
|
expectedFieldName: "metadata.labels.queryField2",
|
||||||
|
})
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "a numeric field should be bracketed",
|
||||||
|
fieldArray: []string{"metadata", "fields", "43"},
|
||||||
|
expectedFieldName: "metadata.fields[43]",
|
||||||
|
})
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "a field starting with a number should be bracketed",
|
||||||
|
fieldArray: []string{"metadata", "fields", "46days"},
|
||||||
|
expectedFieldName: "metadata.fields[46days]",
|
||||||
|
})
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "compound names should be bracketed",
|
||||||
|
fieldArray: []string{"metadata", "labels", "rancher.cattle.io/moo"},
|
||||||
|
expectedFieldName: "metadata.labels[rancher.cattle.io/moo]",
|
||||||
|
})
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "space-separated names should be bracketed",
|
||||||
|
fieldArray: []string{"metadata", "labels", "space here"},
|
||||||
|
expectedFieldName: "metadata.labels[space here]",
|
||||||
|
})
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "already-bracketed terms cause double-bracketing and should never be used",
|
||||||
|
fieldArray: []string{"metadata", "labels[k8s.io/deepcode]"},
|
||||||
|
expectedFieldName: "metadata[labels[k8s.io/deepcode]]",
|
||||||
|
})
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "an empty array should be an empty string",
|
||||||
|
fieldArray: []string{},
|
||||||
|
expectedFieldName: "",
|
||||||
|
})
|
||||||
|
t.Parallel()
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.description, func(t *testing.T) {
|
||||||
|
gotFieldName := smartJoin(test.fieldArray)
|
||||||
|
assert.Equal(t, test.expectedFieldName, gotFieldName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -61,7 +61,7 @@ func (i *IntegrationSuite) TearDownSuite() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *IntegrationSuite) TestSQLCacheFilters() {
|
func (i *IntegrationSuite) TestSQLCacheFilters() {
|
||||||
fields := [][]string{{`metadata`, `annotations[somekey]`}}
|
fields := [][]string{{"metadata", "annotations", "somekey"}}
|
||||||
require := i.Require()
|
require := i.Require()
|
||||||
configMapWithAnnotations := func(name string, annotations map[string]string) v1.ConfigMap {
|
configMapWithAnnotations := func(name string, annotations map[string]string) v1.ConfigMap {
|
||||||
return v1.ConfigMap{
|
return v1.ConfigMap{
|
||||||
@ -122,7 +122,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() {
|
|||||||
{
|
{
|
||||||
name: "matches filter",
|
name: "matches filter",
|
||||||
filters: orFiltersForFilters(informer.Filter{
|
filters: orFiltersForFilters(informer.Filter{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{"somevalue"},
|
Matches: []string{"somevalue"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
Partial: false,
|
Partial: false,
|
||||||
@ -132,7 +132,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() {
|
|||||||
{
|
{
|
||||||
name: "partial matches filter",
|
name: "partial matches filter",
|
||||||
filters: orFiltersForFilters(informer.Filter{
|
filters: orFiltersForFilters(informer.Filter{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{"somevalue"},
|
Matches: []string{"somevalue"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
Partial: true,
|
Partial: true,
|
||||||
@ -142,7 +142,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() {
|
|||||||
{
|
{
|
||||||
name: "no matches for filter with underscore as it is interpreted literally",
|
name: "no matches for filter with underscore as it is interpreted literally",
|
||||||
filters: orFiltersForFilters(informer.Filter{
|
filters: orFiltersForFilters(informer.Filter{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{"somevalu_"},
|
Matches: []string{"somevalu_"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
Partial: true,
|
Partial: true,
|
||||||
@ -152,7 +152,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() {
|
|||||||
{
|
{
|
||||||
name: "no matches for filter with percent sign as it is interpreted literally",
|
name: "no matches for filter with percent sign as it is interpreted literally",
|
||||||
filters: orFiltersForFilters(informer.Filter{
|
filters: orFiltersForFilters(informer.Filter{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{"somevalu%"},
|
Matches: []string{"somevalu%"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
Partial: true,
|
Partial: true,
|
||||||
@ -162,7 +162,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() {
|
|||||||
{
|
{
|
||||||
name: "match with special characters",
|
name: "match with special characters",
|
||||||
filters: orFiltersForFilters(informer.Filter{
|
filters: orFiltersForFilters(informer.Filter{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{"c%%l_value"},
|
Matches: []string{"c%%l_value"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
Partial: true,
|
Partial: true,
|
||||||
@ -172,7 +172,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() {
|
|||||||
{
|
{
|
||||||
name: "match with literal backslash character",
|
name: "match with literal backslash character",
|
||||||
filters: orFiltersForFilters(informer.Filter{
|
filters: orFiltersForFilters(informer.Filter{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{`my\windows\path`},
|
Matches: []string{`my\windows\path`},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
Partial: true,
|
Partial: true,
|
||||||
@ -182,7 +182,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() {
|
|||||||
{
|
{
|
||||||
name: "not eq filter",
|
name: "not eq filter",
|
||||||
filters: orFiltersForFilters(informer.Filter{
|
filters: orFiltersForFilters(informer.Filter{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{"somevalue"},
|
Matches: []string{"somevalue"},
|
||||||
Op: informer.NotEq,
|
Op: informer.NotEq,
|
||||||
Partial: false,
|
Partial: false,
|
||||||
@ -192,7 +192,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() {
|
|||||||
{
|
{
|
||||||
name: "partial not eq filter",
|
name: "partial not eq filter",
|
||||||
filters: orFiltersForFilters(informer.Filter{
|
filters: orFiltersForFilters(informer.Filter{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{"somevalue"},
|
Matches: []string{"somevalue"},
|
||||||
Op: informer.NotEq,
|
Op: informer.NotEq,
|
||||||
Partial: true,
|
Partial: true,
|
||||||
@ -203,13 +203,13 @@ func (i *IntegrationSuite) TestSQLCacheFilters() {
|
|||||||
name: "multiple or filters match",
|
name: "multiple or filters match",
|
||||||
filters: orFiltersForFilters(
|
filters: orFiltersForFilters(
|
||||||
informer.Filter{
|
informer.Filter{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{"somevalue"},
|
Matches: []string{"somevalue"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
Partial: true,
|
Partial: true,
|
||||||
},
|
},
|
||||||
informer.Filter{
|
informer.Filter{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{"notequal"},
|
Matches: []string{"notequal"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
Partial: false,
|
Partial: false,
|
||||||
@ -221,7 +221,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() {
|
|||||||
name: "or filters on different fields",
|
name: "or filters on different fields",
|
||||||
filters: orFiltersForFilters(
|
filters: orFiltersForFilters(
|
||||||
informer.Filter{
|
informer.Filter{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{"somevalue"},
|
Matches: []string{"somevalue"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
Partial: true,
|
Partial: true,
|
||||||
@ -241,7 +241,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() {
|
|||||||
{
|
{
|
||||||
Filters: []informer.Filter{
|
Filters: []informer.Filter{
|
||||||
{
|
{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{"somevalue"},
|
Matches: []string{"somevalue"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
Partial: true,
|
Partial: true,
|
||||||
@ -265,7 +265,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() {
|
|||||||
name: "no matches",
|
name: "no matches",
|
||||||
filters: orFiltersForFilters(
|
filters: orFiltersForFilters(
|
||||||
informer.Filter{
|
informer.Filter{
|
||||||
Field: []string{`metadata`, `annotations[somekey]`},
|
Field: []string{"metadata", "annotations", "somekey"},
|
||||||
Matches: []string{"valueNotRepresented"},
|
Matches: []string{"valueNotRepresented"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
Partial: false,
|
Partial: false,
|
||||||
|
@ -35,7 +35,7 @@ const (
|
|||||||
notOp = "!"
|
notOp = "!"
|
||||||
)
|
)
|
||||||
|
|
||||||
var labelsRegex = regexp.MustCompile(`^(metadata)\.(labels)\[(.+)\]$`)
|
var endsWithBracket = regexp.MustCompile(`^(.+)\[(.+)]$`)
|
||||||
var mapK8sOpToRancherOp = map[selection.Operator]informer.Op{
|
var mapK8sOpToRancherOp = map[selection.Operator]informer.Op{
|
||||||
selection.Equals: informer.Eq,
|
selection.Equals: informer.Eq,
|
||||||
selection.DoubleEquals: informer.Eq,
|
selection.DoubleEquals: informer.Eq,
|
||||||
@ -191,17 +191,16 @@ func getLimit(apiOp *types.APIRequest) int {
|
|||||||
return limit
|
return limit
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitQuery takes a single-string metadata-labels filter and converts it into an array of 3 accessor strings,
|
// splitQuery takes a single-string k8s object accessor and returns its separate fields in a slice.
|
||||||
// where the first two strings are always "metadata" and "labels", and the third is the label name.
|
// "Simple" accessors of the form `metadata.labels.foo` => ["metadata", "labels", "foo"]
|
||||||
// This is more complex than doing something like `strings.Split(".", "metadata.labels.fieldName")
|
// but accessors with square brackets need to be broken on the brackets, as in
|
||||||
// because the fieldName can be more complex - in particular it can contain "."s) and needs to be
|
// "metadata.annotations[k8s.io/this-is-fun]" => ["metadata", "annotations", "k8s.io/this-is-fun"]
|
||||||
// bracketed, as in `metadata.labels[rancher.io/cattle.and.beef]".
|
// We assume in the kubernetes/rancher world json keys are always alphanumeric-underscorish, so
|
||||||
// The `labelsRegex` looks for the bracketed form.
|
// we only look for square brackets at the end of the string.
|
||||||
func splitQuery(query string) []string {
|
func splitQuery(query string) []string {
|
||||||
m := labelsRegex.FindStringSubmatch(query)
|
m := endsWithBracket.FindStringSubmatch(query)
|
||||||
if m != nil && len(m) == 4 {
|
if m != nil {
|
||||||
// m[0] contains the entire string, so just return all but that first item in `m`
|
return append(strings.Split(m[1], "."), m[2])
|
||||||
return m[1:]
|
|
||||||
}
|
}
|
||||||
return strings.Split(query, ".")
|
return strings.Split(query, ".")
|
||||||
}
|
}
|
||||||
@ -219,7 +218,7 @@ func parseNamespaceOrProjectFilters(ctx context.Context, projOrNS string, op inf
|
|||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
|
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
|
||||||
Matches: []string{pn},
|
Matches: []string{pn},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
},
|
},
|
||||||
|
@ -92,7 +92,7 @@ func TestParseQuery(t *testing.T) {
|
|||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
|
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
|
||||||
Matches: []string{"somethin"},
|
Matches: []string{"somethin"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
},
|
},
|
||||||
@ -142,7 +142,7 @@ func TestParseQuery(t *testing.T) {
|
|||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
|
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
|
||||||
Matches: []string{"somethin"},
|
Matches: []string{"somethin"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
},
|
},
|
||||||
@ -195,7 +195,7 @@ func TestParseQuery(t *testing.T) {
|
|||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
|
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
|
||||||
Matches: []string{"somethin"},
|
Matches: []string{"somethin"},
|
||||||
Op: informer.Eq,
|
Op: informer.Eq,
|
||||||
},
|
},
|
||||||
@ -293,6 +293,83 @@ func TestParseQuery(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "ParseQuery() with an annotations filter param should split it correctly.",
|
||||||
|
req: &types.APIRequest{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: &url.URL{RawQuery: "filter=metadata.annotations[chumley.example.com/fish]=seals"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedLO: informer.ListOptions{
|
||||||
|
ChunkSize: defaultLimit,
|
||||||
|
Filters: []informer.OrFilter{
|
||||||
|
{
|
||||||
|
Filters: []informer.Filter{
|
||||||
|
{
|
||||||
|
Field: []string{"metadata", "annotations", "chumley.example.com/fish"},
|
||||||
|
Matches: []string{"seals"},
|
||||||
|
Op: informer.Eq,
|
||||||
|
Partial: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Pagination: informer.Pagination{
|
||||||
|
Page: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "ParseQuery() with a numeric filter index should split it correctly.",
|
||||||
|
req: &types.APIRequest{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: &url.URL{RawQuery: "filter=metadata.fields[3]<5"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedLO: informer.ListOptions{
|
||||||
|
ChunkSize: defaultLimit,
|
||||||
|
Filters: []informer.OrFilter{
|
||||||
|
{
|
||||||
|
Filters: []informer.Filter{
|
||||||
|
{
|
||||||
|
Field: []string{"metadata", "fields", "3"},
|
||||||
|
Matches: []string{"5"},
|
||||||
|
Op: informer.Lt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Pagination: informer.Pagination{
|
||||||
|
Page: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
tests = append(tests, testCase{
|
||||||
|
description: "ParseQuery() with a labels filter param should create a labels-specific filter.",
|
||||||
|
req: &types.APIRequest{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: &url.URL{RawQuery: "filter=metadata.labels[grover.example.com/fish]~heads"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedLO: informer.ListOptions{
|
||||||
|
ChunkSize: defaultLimit,
|
||||||
|
Filters: []informer.OrFilter{
|
||||||
|
{
|
||||||
|
Filters: []informer.Filter{
|
||||||
|
{
|
||||||
|
Field: []string{"metadata", "labels", "grover.example.com/fish"},
|
||||||
|
Matches: []string{"heads"},
|
||||||
|
Op: informer.Eq,
|
||||||
|
Partial: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Pagination: informer.Pagination{
|
||||||
|
Page: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
tests = append(tests, testCase{
|
tests = append(tests, testCase{
|
||||||
description: "ParseQuery() with multiple filter params, should include multiple or filters.",
|
description: "ParseQuery() with multiple filter params, should include multiple or filters.",
|
||||||
req: &types.APIRequest{
|
req: &types.APIRequest{
|
||||||
|
@ -6,8 +6,8 @@ import (
|
|||||||
"go.uber.org/mock/gomock"
|
"go.uber.org/mock/gomock"
|
||||||
|
|
||||||
"github.com/rancher/apiserver/pkg/types"
|
"github.com/rancher/apiserver/pkg/types"
|
||||||
"github.com/rancher/steve/pkg/sqlcache/partition"
|
|
||||||
"github.com/rancher/steve/pkg/accesscontrol"
|
"github.com/rancher/steve/pkg/accesscontrol"
|
||||||
|
"github.com/rancher/steve/pkg/sqlcache/partition"
|
||||||
"github.com/rancher/wrangler/v3/pkg/schemas"
|
"github.com/rancher/wrangler/v3/pkg/schemas"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
@ -15,8 +15,8 @@ import (
|
|||||||
"go.uber.org/mock/gomock"
|
"go.uber.org/mock/gomock"
|
||||||
|
|
||||||
"github.com/rancher/apiserver/pkg/types"
|
"github.com/rancher/apiserver/pkg/types"
|
||||||
"github.com/rancher/steve/pkg/sqlcache/partition"
|
|
||||||
"github.com/rancher/steve/pkg/accesscontrol"
|
"github.com/rancher/steve/pkg/accesscontrol"
|
||||||
|
"github.com/rancher/steve/pkg/sqlcache/partition"
|
||||||
"github.com/rancher/steve/pkg/stores/sqlproxy"
|
"github.com/rancher/steve/pkg/stores/sqlproxy"
|
||||||
"github.com/rancher/wrangler/v3/pkg/generic"
|
"github.com/rancher/wrangler/v3/pkg/generic"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
@ -62,7 +62,7 @@ var (
|
|||||||
// Please keep the gvkKey entries in alphabetical order, on a field-by-field basis
|
// Please keep the gvkKey entries in alphabetical order, on a field-by-field basis
|
||||||
typeSpecificIndexedFields = map[string][][]string{
|
typeSpecificIndexedFields = map[string][][]string{
|
||||||
gvkKey("", "v1", "ConfigMap"): {
|
gvkKey("", "v1", "ConfigMap"): {
|
||||||
{"metadata", "labels[harvesterhci.io/cloud-init-template]"}},
|
{"metadata", "labels", "harvesterhci.io/cloud-init-template"}},
|
||||||
gvkKey("", "v1", "Event"): {
|
gvkKey("", "v1", "Event"): {
|
||||||
{"_type"},
|
{"_type"},
|
||||||
{"involvedObject", "kind"},
|
{"involvedObject", "kind"},
|
||||||
@ -71,7 +71,7 @@ var (
|
|||||||
{"reason"},
|
{"reason"},
|
||||||
},
|
},
|
||||||
gvkKey("", "v1", "Namespace"): {
|
gvkKey("", "v1", "Namespace"): {
|
||||||
{"metadata", "labels[field.cattle.io/projectId]"}},
|
{"metadata", "labels", "field.cattle.io/projectId"}},
|
||||||
gvkKey("", "v1", "Node"): {
|
gvkKey("", "v1", "Node"): {
|
||||||
{"status", "nodeInfo", "kubeletVersion"},
|
{"status", "nodeInfo", "kubeletVersion"},
|
||||||
{"status", "nodeInfo", "operatingSystem"}},
|
{"status", "nodeInfo", "operatingSystem"}},
|
||||||
@ -89,13 +89,13 @@ var (
|
|||||||
{"spec", "type"},
|
{"spec", "type"},
|
||||||
},
|
},
|
||||||
gvkKey("apps", "v1", "DaemonSet"): {
|
gvkKey("apps", "v1", "DaemonSet"): {
|
||||||
{"metadata", "annotations[field.cattle.io/publicEndpoints]"},
|
{"metadata", "annotations", "field.cattle.io/publicEndpoints"},
|
||||||
},
|
},
|
||||||
gvkKey("apps", "v1", "Deployment"): {
|
gvkKey("apps", "v1", "Deployment"): {
|
||||||
{"metadata", "annotations[field.cattle.io/publicEndpoints]"},
|
{"metadata", "annotations", "field.cattle.io/publicEndpoints"},
|
||||||
},
|
},
|
||||||
gvkKey("apps", "v1", "StatefulSet"): {
|
gvkKey("apps", "v1", "StatefulSet"): {
|
||||||
{"metadata", "annotations[field.cattle.io/publicEndpoints]"},
|
{"metadata", "annotations", "field.cattle.io/publicEndpoints"},
|
||||||
},
|
},
|
||||||
gvkKey("autoscaling", "v2", "HorizontalPodAutoscaler"): {
|
gvkKey("autoscaling", "v2", "HorizontalPodAutoscaler"): {
|
||||||
{"spec", "scaleTargetRef", "name"},
|
{"spec", "scaleTargetRef", "name"},
|
||||||
@ -104,16 +104,16 @@ var (
|
|||||||
{"status", "currentReplicas"},
|
{"status", "currentReplicas"},
|
||||||
},
|
},
|
||||||
gvkKey("batch", "v1", "CronJob"): {
|
gvkKey("batch", "v1", "CronJob"): {
|
||||||
{"metadata", "annotations[field.cattle.io/publicEndpoints]"},
|
{"metadata", "annotations", "field.cattle.io/publicEndpoints"},
|
||||||
},
|
},
|
||||||
gvkKey("batch", "v1", "Job"): {
|
gvkKey("batch", "v1", "Job"): {
|
||||||
{"metadata", "annotations[field.cattle.io/publicEndpoints]"},
|
{"metadata", "annotations", "field.cattle.io/publicEndpoints"},
|
||||||
},
|
},
|
||||||
gvkKey("catalog.cattle.io", "v1", "App"): {
|
gvkKey("catalog.cattle.io", "v1", "App"): {
|
||||||
{"spec", "chart", "metadata", "name"},
|
{"spec", "chart", "metadata", "name"},
|
||||||
},
|
},
|
||||||
gvkKey("catalog.cattle.io", "v1", "ClusterRepo"): {
|
gvkKey("catalog.cattle.io", "v1", "ClusterRepo"): {
|
||||||
{"metadata", "annotations[clusterrepo.cattle.io/hidden]"},
|
{"metadata", "annotations", "clusterrepo.cattle.io/hidden"},
|
||||||
{"spec", "gitBranch"},
|
{"spec", "gitBranch"},
|
||||||
{"spec", "gitRepo"},
|
{"spec", "gitRepo"},
|
||||||
},
|
},
|
||||||
@ -125,7 +125,7 @@ var (
|
|||||||
gvkKey("cluster.x-k8s.io", "v1beta1", "Machine"): {
|
gvkKey("cluster.x-k8s.io", "v1beta1", "Machine"): {
|
||||||
{"spec", "clusterName"}},
|
{"spec", "clusterName"}},
|
||||||
gvkKey("management.cattle.io", "v3", "Cluster"): {
|
gvkKey("management.cattle.io", "v3", "Cluster"): {
|
||||||
{"metadata", "labels[provider.cattle.io]"},
|
{"metadata", "labels", "provider.cattle.io"},
|
||||||
{"spec", "internal"},
|
{"spec", "internal"},
|
||||||
{"spec", "displayName"},
|
{"spec", "displayName"},
|
||||||
{"status", "connected"},
|
{"status", "connected"},
|
||||||
@ -142,13 +142,13 @@ var (
|
|||||||
{"spec", "ingressClassName"},
|
{"spec", "ingressClassName"},
|
||||||
},
|
},
|
||||||
gvkKey("provisioning.cattle.io", "v1", "Cluster"): {
|
gvkKey("provisioning.cattle.io", "v1", "Cluster"): {
|
||||||
{"metadata", "labels[provider.cattle.io]"},
|
{"metadata", "labels", "provider.cattle.io"},
|
||||||
{"status", "clusterName"},
|
{"status", "clusterName"},
|
||||||
{"status", "provider"},
|
{"status", "provider"},
|
||||||
},
|
},
|
||||||
gvkKey("storage.k8s.io", "v1", "StorageClass"): {
|
gvkKey("storage.k8s.io", "v1", "StorageClass"): {
|
||||||
{"provisioner"},
|
{"provisioner"},
|
||||||
{"metadata", "annotations[storageclass.kubernetes.io/is-default-class]"},
|
{"metadata", "annotations", "storageclass.kubernetes.io/is-default-class"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
commonIndexFields = [][]string{
|
commonIndexFields = [][]string{
|
||||||
|
@ -82,7 +82,7 @@ func TestNewProxyStore(t *testing.T) {
|
|||||||
nsSchema := baseNSSchema
|
nsSchema := baseNSSchema
|
||||||
scc.EXPECT().SetColumns(context.Background(), &nsSchema).Return(nil)
|
scc.EXPECT().SetColumns(context.Background(), &nsSchema).Return(nil)
|
||||||
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
|
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
|
||||||
cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels[field.cattle.io/projectId]"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(c, nil)
|
cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels", "field.cattle.io/projectId"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(c, nil)
|
||||||
|
|
||||||
s, err := NewProxyStore(context.Background(), scc, cg, rn, nil, cf)
|
s, err := NewProxyStore(context.Background(), scc, cg, rn, nil, cf)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -149,7 +149,7 @@ func TestNewProxyStore(t *testing.T) {
|
|||||||
nsSchema := baseNSSchema
|
nsSchema := baseNSSchema
|
||||||
scc.EXPECT().SetColumns(context.Background(), &nsSchema).Return(nil)
|
scc.EXPECT().SetColumns(context.Background(), &nsSchema).Return(nil)
|
||||||
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
|
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
|
||||||
cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels[field.cattle.io/projectId]"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(factory.Cache{}, fmt.Errorf("error"))
|
cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels", "field.cattle.io/projectId"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(factory.Cache{}, fmt.Errorf("error"))
|
||||||
|
|
||||||
s, err := NewProxyStore(context.Background(), scc, cg, rn, nil, cf)
|
s, err := NewProxyStore(context.Background(), scc, cg, rn, nil, cf)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -652,7 +652,7 @@ func TestReset(t *testing.T) {
|
|||||||
cf.EXPECT().Reset().Return(nil)
|
cf.EXPECT().Reset().Return(nil)
|
||||||
cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil)
|
cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil)
|
||||||
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
|
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
|
||||||
cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels[field.cattle.io/projectId]"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(nsc2, nil)
|
cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels", "field.cattle.io/projectId"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(nsc2, nil)
|
||||||
tb.EXPECT().GetTransformFunc(attributes.GVK(&nsSchema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
tb.EXPECT().GetTransformFunc(attributes.GVK(&nsSchema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
||||||
err := s.Reset()
|
err := s.Reset()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -759,7 +759,7 @@ func TestReset(t *testing.T) {
|
|||||||
cf.EXPECT().Reset().Return(nil)
|
cf.EXPECT().Reset().Return(nil)
|
||||||
cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil)
|
cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil)
|
||||||
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
|
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
|
||||||
cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels[field.cattle.io/projectId]"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(factory.Cache{}, fmt.Errorf("error"))
|
cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels", "field.cattle.io/projectId"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(factory.Cache{}, fmt.Errorf("error"))
|
||||||
tb.EXPECT().GetTransformFunc(attributes.GVK(&nsSchema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
tb.EXPECT().GetTransformFunc(attributes.GVK(&nsSchema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
||||||
err := s.Reset()
|
err := s.Reset()
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
|
Loading…
Reference in New Issue
Block a user