1
0
mirror of https://github.com/rancher/steve.git synced 2025-09-05 01:12:09 +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:
Eric Promislow
2025-02-25 10:39:29 -08:00
committed by GitHub
parent 527d44a3a7
commit 3b45729415
9 changed files with 228 additions and 49 deletions

View File

@@ -35,7 +35,7 @@ const (
notOp = "!"
)
var labelsRegex = regexp.MustCompile(`^(metadata)\.(labels)\[(.+)\]$`)
var endsWithBracket = regexp.MustCompile(`^(.+)\[(.+)]$`)
var mapK8sOpToRancherOp = map[selection.Operator]informer.Op{
selection.Equals: informer.Eq,
selection.DoubleEquals: informer.Eq,
@@ -191,17 +191,16 @@ func getLimit(apiOp *types.APIRequest) int {
return limit
}
// splitQuery takes a single-string metadata-labels filter and converts it into an array of 3 accessor strings,
// where the first two strings are always "metadata" and "labels", and the third is the label name.
// This is more complex than doing something like `strings.Split(".", "metadata.labels.fieldName")
// because the fieldName can be more complex - in particular it can contain "."s) and needs to be
// bracketed, as in `metadata.labels[rancher.io/cattle.and.beef]".
// The `labelsRegex` looks for the bracketed form.
// splitQuery takes a single-string k8s object accessor and returns its separate fields in a slice.
// "Simple" accessors of the form `metadata.labels.foo` => ["metadata", "labels", "foo"]
// but accessors with square brackets need to be broken on the brackets, as in
// "metadata.annotations[k8s.io/this-is-fun]" => ["metadata", "annotations", "k8s.io/this-is-fun"]
// We assume in the kubernetes/rancher world json keys are always alphanumeric-underscorish, so
// we only look for square brackets at the end of the string.
func splitQuery(query string) []string {
m := labelsRegex.FindStringSubmatch(query)
if m != nil && len(m) == 4 {
// m[0] contains the entire string, so just return all but that first item in `m`
return m[1:]
m := endsWithBracket.FindStringSubmatch(query)
if m != nil {
return append(strings.Split(m[1], "."), m[2])
}
return strings.Split(query, ".")
}
@@ -219,7 +218,7 @@ func parseNamespaceOrProjectFilters(ctx context.Context, projOrNS string, op inf
Op: informer.Eq,
},
{
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{pn},
Op: informer.Eq,
},

View File

@@ -92,7 +92,7 @@ func TestParseQuery(t *testing.T) {
Op: informer.Eq,
},
{
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{"somethin"},
Op: informer.Eq,
},
@@ -142,7 +142,7 @@ func TestParseQuery(t *testing.T) {
Op: informer.Eq,
},
{
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{"somethin"},
Op: informer.Eq,
},
@@ -195,7 +195,7 @@ func TestParseQuery(t *testing.T) {
Op: informer.Eq,
},
{
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{"somethin"},
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{
description: "ParseQuery() with multiple filter params, should include multiple or filters.",
req: &types.APIRequest{