1
0
mirror of https://github.com/rancher/steve.git synced 2025-09-12 21:39:30 +00:00

#50968 - Single SQL Transaction for projectsornamespaces filter (#758)

* working for positive case

* changed to custom code

* removed comment

* added tests

* fixing ci error

* one more fix

* Update proxy_store tests for projectornamespace parsing.

Since we no longer need to execute a db query, and just return a filter,
we can drop a lot of code.

* added distinct back to conform with other queries

* one more fix

---------

Co-authored-by: Eric Promislow <epromislow@suse.com>
This commit is contained in:
Felipe Gehrke
2025-08-08 16:07:54 -03:00
committed by GitHub
parent dbd2818d22
commit 3cac88141b
7 changed files with 324 additions and 300 deletions

View File

@@ -73,8 +73,10 @@ var (
subfieldRegex = regexp.MustCompile(`([a-zA-Z]+)|(\[[-a-zA-Z./]+])|(\[[0-9]+])`)
containsNonNumericRegex = regexp.MustCompile(`\D`)
ErrInvalidColumn = errors.New("supplied column is invalid")
ErrTooOld = errors.New("resourceversion too old")
ErrInvalidColumn = errors.New("supplied column is invalid")
ErrTooOld = errors.New("resourceversion too old")
projectIDFieldLabel = "field.cattle.io/projectId"
namespacesDbName = "_v1_Namespace"
)
const (
@@ -632,7 +634,7 @@ type QueryInfo struct {
func (l *ListOptionIndexer) constructQuery(lo *sqltypes.ListOptions, partitions []partition.Partition, namespace string, dbName string) (*QueryInfo, error) {
unboundSortLabels := getUnboundSortLabels(lo)
queryInfo := &QueryInfo{}
queryUsesLabels := hasLabelFilter(lo.Filters)
queryUsesLabels := hasLabelFilter(lo.Filters) || len(lo.ProjectsOrNamespaces.Filters) > 0
joinTableIndexByLabelName := make(map[string]int)
// First, what kind of filtering will we be doing?
@@ -681,6 +683,26 @@ func (l *ListOptionIndexer) constructQuery(lo *sqltypes.ListOptions, partitions
}
}
}
}
if len(lo.ProjectsOrNamespaces.Filters) > 0 {
jtIndex := len(joinTableIndexByLabelName) + 1
if _, exists := joinTableIndexByLabelName[projectIDFieldLabel]; !exists {
joinTableIndexByLabelName[projectIDFieldLabel] = jtIndex
}
// if we're looking for ProjectsOrNamespaces while querying for _v1_Namespaces (not very usual, but possible),
// we need to change the field prefix from 'nsf' which means namespaces fields to 'f' which is the default join
// name for every object, also we do not need to join namespaces again
fieldPrefix := "f"
if dbName != namespacesDbName {
fieldPrefix = "nsf"
query += "\n "
query += fmt.Sprintf(`JOIN "%s_fields" nsf ON f."metadata.namespace" = nsf."metadata.name"`, namespacesDbName)
}
query += "\n "
query += fmt.Sprintf(`LEFT OUTER JOIN "%s_labels" lt%d ON %s.key = lt%d.key`, namespacesDbName, jtIndex, fieldPrefix, jtIndex)
}
// 2- Filtering: WHERE clauses (from lo.Filters)
@@ -696,6 +718,20 @@ func (l *ListOptionIndexer) constructQuery(lo *sqltypes.ListOptions, partitions
params = append(params, orParams...)
}
// WHERE clauses (from lo.ProjectsOrNamespaces)
if len(lo.ProjectsOrNamespaces.Filters) > 0 {
fieldPrefix := "nsf"
if dbName == namespacesDbName {
fieldPrefix = "f"
}
projOrNsClause, projOrNsParams, err := l.buildClauseFromProjectsOrNamespaces(lo.ProjectsOrNamespaces, dbName, joinTableIndexByLabelName, fieldPrefix)
if err != nil {
return queryInfo, err
}
whereClauses = append(whereClauses, projOrNsClause)
params = append(params, projOrNsParams...)
}
// WHERE clauses (from namespace)
if namespace != "" && namespace != "*" {
whereClauses = append(whereClauses, fmt.Sprintf(`f."metadata.namespace" = ?`))
@@ -802,7 +838,6 @@ func (l *ListOptionIndexer) constructQuery(lo *sqltypes.ListOptions, partitions
}
// 4- Pagination: LIMIT clause (from lo.Pagination)
limitClause := ""
limit := lo.Pagination.PageSize
if limit > 0 {
@@ -982,7 +1017,7 @@ func (l *ListOptionIndexer) buildORClauseFromFilters(orFilters sqltypes.OrFilter
}
newClause, newParams, err = l.getLabelFilter(index, filter, dbName)
} else {
newClause, newParams, err = l.getFieldFilter(filter)
newClause, newParams, err = l.getFieldFilter(filter, "f")
}
if err != nil {
return "", nil, err
@@ -999,6 +1034,46 @@ func (l *ListOptionIndexer) buildORClauseFromFilters(orFilters sqltypes.OrFilter
return fmt.Sprintf("(%s)", strings.Join(clauses, ") OR (")), params, nil
}
func (l *ListOptionIndexer) buildClauseFromProjectsOrNamespaces(orFilters sqltypes.OrFilter, dbName string, joinTableIndexByLabelName map[string]int, fieldPrefix string) (string, []any, error) {
var params []any
var newParams []any
var newClause string
var err error
var index int
if len(orFilters.Filters) == 0 {
return "", params, nil
}
clauses := make([]string, 0, len(orFilters.Filters))
for _, filter := range orFilters.Filters {
if isLabelFilter(&filter) {
if index, err = internLabel(filter.Field[2], joinTableIndexByLabelName, -1); err != nil {
return "", nil, err
}
newClause, newParams, err = l.getProjectsOrNamespacesLabelFilter(index, filter, dbName)
} else {
newClause, newParams, err = l.getProjectsOrNamespacesFieldFilter(filter, fieldPrefix)
}
if err != nil {
return "", nil, err
}
clauses = append(clauses, newClause)
params = append(params, newParams...)
}
if orFilters.Filters[0].Op == sqltypes.In {
return fmt.Sprintf("(%s)", strings.Join(clauses, ") OR (")), params, nil
}
if orFilters.Filters[0].Op == sqltypes.NotIn {
return fmt.Sprintf("(%s)", strings.Join(clauses, ") AND (")), params, nil
}
return "", nil, fmt.Errorf("project or namespaces supports only 'IN' or 'NOT IN' operation. op: %s is not valid",
orFilters.Filters[0].Op)
}
func buildSortLabelsClause(labelName string, joinTableIndexByLabelName map[string]int, isAsc bool) (string, error) {
ltIndex, err := internLabel(labelName, joinTableIndexByLabelName, -1)
if err != nil {
@@ -1086,10 +1161,10 @@ func internLabel(labelName string, joinTableIndexByLabelName map[string]int, nex
// KEY in VALUES
// KEY notin VALUES
func (l *ListOptionIndexer) getFieldFilter(filter sqltypes.Filter) (string, []any, error) {
func (l *ListOptionIndexer) getFieldFilter(filter sqltypes.Filter, prefix string) (string, []any, error) {
opString := ""
escapeString := ""
fieldEntry, err := l.getValidFieldEntry("f", filter.Field)
fieldEntry, err := l.getValidFieldEntry(prefix, filter.Field)
if err != nil {
return "", nil, err
}
@@ -1146,6 +1221,61 @@ func (l *ListOptionIndexer) getFieldFilter(filter sqltypes.Filter) (string, []an
return "", nil, fmt.Errorf("unrecognized operator: %s", opString)
}
func (l *ListOptionIndexer) getProjectsOrNamespacesFieldFilter(filter sqltypes.Filter, prefix string) (string, []any, error) {
opString := ""
fieldEntry, err := l.getValidFieldEntry(prefix, filter.Field)
if err != nil {
return "", nil, err
}
switch filter.Op {
case sqltypes.In:
fallthrough
case sqltypes.NotIn:
target := "()"
if len(filter.Matches) > 0 {
target = fmt.Sprintf("(?%s)", strings.Repeat(", ?", len(filter.Matches)-1))
}
opString = "IN"
if filter.Op == sqltypes.NotIn {
opString = "NOT IN"
}
clause := fmt.Sprintf("%s %s %s", fieldEntry, opString, target)
matches := make([]any, len(filter.Matches))
for i, match := range filter.Matches {
matches[i] = match
}
return clause, matches, nil
}
return "", nil, fmt.Errorf("unrecognized operator: %s", opString)
}
func (l *ListOptionIndexer) getProjectsOrNamespacesLabelFilter(index int, filter sqltypes.Filter, dbName string) (string, []any, error) {
opString := ""
labelName := filter.Field[2]
switch filter.Op {
case sqltypes.In:
fallthrough
case sqltypes.NotIn:
opString = "IN"
if filter.Op == sqltypes.NotIn {
opString = "NOT IN"
}
target := "()"
if len(filter.Matches) > 0 {
target = fmt.Sprintf("(?%s)", strings.Repeat(", ?", len(filter.Matches)-1))
}
clause := fmt.Sprintf(`lt%d.label = ? AND lt%d.value %s %s`, index, index, opString, target)
matches := make([]any, len(filter.Matches)+1)
matches[0] = labelName
for i, match := range filter.Matches {
matches[i+1] = match
}
return clause, matches, nil
}
return "", nil, fmt.Errorf("unrecognized operator: %s", opString)
}
func (l *ListOptionIndexer) getLabelFilter(index int, filter sqltypes.Filter, dbName string) (string, []any, error) {
opString := ""
escapeString := ""

View File

@@ -1528,6 +1528,138 @@ func TestConstructQuery(t *testing.T) {
expectedStmtArgs: []any{"somevalue"},
expectedErr: nil,
})
tests = append(tests, testCase{
description: "TestConstructQuery: handles ProjectOrNamespaces IN",
listOptions: sqltypes.ListOptions{
ProjectsOrNamespaces: sqltypes.OrFilter{
Filters: []sqltypes.Filter{
sqltypes.Filter{
Field: []string{"metadata", "name"},
Matches: []string{"some_namespace"},
Op: sqltypes.In,
},
sqltypes.Filter{
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{"some_namespace"},
Op: sqltypes.In,
},
},
},
Filters: []sqltypes.OrFilter{},
},
partitions: []partition.Partition{
{
All: true,
},
},
ns: "",
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
JOIN "something_fields" f ON o.key = f.key
JOIN "_v1_Namespace_fields" nsf ON f."metadata.namespace" = nsf."metadata.name"
LEFT OUTER JOIN "_v1_Namespace_labels" lt1 ON nsf.key = lt1.key
WHERE
((nsf."metadata.name" IN (?)) OR (lt1.label = ? AND lt1.value IN (?)))
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"some_namespace", "field.cattle.io/projectId", "some_namespace"},
expectedErr: nil,
})
tests = append(tests, testCase{
description: "TestConstructQuery: handles ProjectOrNamespaces multiple IN",
listOptions: sqltypes.ListOptions{
ProjectsOrNamespaces: sqltypes.OrFilter{
Filters: []sqltypes.Filter{
sqltypes.Filter{
Field: []string{"metadata", "name"},
Matches: []string{"some_namespace", "p-example"},
Op: sqltypes.In,
},
sqltypes.Filter{
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{"some_namespace", "p-example"},
Op: sqltypes.In,
},
},
},
Filters: []sqltypes.OrFilter{},
},
partitions: []partition.Partition{
{
All: true,
},
},
ns: "",
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
JOIN "something_fields" f ON o.key = f.key
JOIN "_v1_Namespace_fields" nsf ON f."metadata.namespace" = nsf."metadata.name"
LEFT OUTER JOIN "_v1_Namespace_labels" lt1 ON nsf.key = lt1.key
WHERE
((nsf."metadata.name" IN (?, ?)) OR (lt1.label = ? AND lt1.value IN (?, ?)))
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"some_namespace", "p-example", "field.cattle.io/projectId", "some_namespace", "p-example"},
expectedErr: nil,
})
tests = append(tests, testCase{
description: "TestConstructQuery: handles ProjectOrNamespaces NOT IN",
listOptions: sqltypes.ListOptions{
ProjectsOrNamespaces: sqltypes.OrFilter{
Filters: []sqltypes.Filter{
sqltypes.Filter{
Field: []string{"metadata", "name"},
Matches: []string{"some_namespace"},
Op: sqltypes.NotIn,
},
sqltypes.Filter{
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{"some_namespace"},
Op: sqltypes.NotIn,
},
},
},
Filters: []sqltypes.OrFilter{},
},
partitions: []partition.Partition{{All: true}},
ns: "",
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
JOIN "something_fields" f ON o.key = f.key
JOIN "_v1_Namespace_fields" nsf ON f."metadata.namespace" = nsf."metadata.name"
LEFT OUTER JOIN "_v1_Namespace_labels" lt1 ON nsf.key = lt1.key
WHERE
((nsf."metadata.name" NOT IN (?)) AND (lt1.label = ? AND lt1.value NOT IN (?)))
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"some_namespace", "field.cattle.io/projectId", "some_namespace"},
expectedErr: nil,
})
tests = append(tests, testCase{
description: "TestConstructQuery: handles ProjectOrNamespaces multiple NOT IN",
listOptions: sqltypes.ListOptions{
ProjectsOrNamespaces: sqltypes.OrFilter{
Filters: []sqltypes.Filter{
sqltypes.Filter{
Field: []string{"metadata", "name"},
Matches: []string{"some_namespace", "p-example"},
Op: sqltypes.NotIn,
},
sqltypes.Filter{
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{"some_namespace", "p-example"},
Op: sqltypes.NotIn,
},
},
},
Filters: []sqltypes.OrFilter{},
},
partitions: []partition.Partition{{All: true}},
ns: "",
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
JOIN "something_fields" f ON o.key = f.key
JOIN "_v1_Namespace_fields" nsf ON f."metadata.namespace" = nsf."metadata.name"
LEFT OUTER JOIN "_v1_Namespace_labels" lt1 ON nsf.key = lt1.key
WHERE
((nsf."metadata.name" NOT IN (?, ?)) AND (lt1.label = ? AND lt1.value NOT IN (?, ?)))
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"some_namespace", "p-example", "field.cattle.io/projectId", "some_namespace", "p-example"},
expectedErr: nil,
})
tests = append(tests, testCase{
description: "TestConstructQuery: handles EXISTS statements",
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
@@ -2176,7 +2308,7 @@ SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
}
lii := &ListOptionIndexer{
Indexer: i,
indexedFields: []string{"metadata.queryField1", "status.queryField2", "spec.containers.image"},
indexedFields: []string{"metadata.queryField1", "status.queryField2", "spec.containers.image", "metadata.name"},
}
queryInfo, err := lii.constructQuery(&test.listOptions, test.partitions, test.ns, "something")
if test.expectedErr != nil {

View File

@@ -27,9 +27,10 @@ const (
// ListOptions represents the query parameters that may be included in a list request.
type ListOptions struct {
Filters []OrFilter
SortList SortList
Pagination Pagination
Filters []OrFilter
ProjectsOrNamespaces OrFilter
SortList SortList
Pagination Pagination
}
// Filter represents a field to filter by.

View File

@@ -4,19 +4,16 @@ package listprocessor
import (
"context"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"github.com/rancher/apiserver/pkg/apierror"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/sqlcache/partition"
"github.com/rancher/steve/pkg/sqlcache/sqltypes"
"github.com/rancher/steve/pkg/stores/queryhelper"
"github.com/rancher/steve/pkg/stores/sqlpartition/queryparser"
"github.com/rancher/steve/pkg/stores/sqlpartition/selection"
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@@ -80,7 +77,7 @@ func k8sRequirementToOrFilter(requirement queryparser.Requirement) (sqltypes.Fil
}
// ParseQuery parses the query params of a request and returns a ListOptions.
func ParseQuery(apiOp *types.APIRequest, namespaceCache Cache) (sqltypes.ListOptions, error) {
func ParseQuery(apiOp *types.APIRequest) (sqltypes.ListOptions, error) {
opts := sqltypes.ListOptions{}
q := apiOp.Request.URL.Query()
@@ -140,30 +137,16 @@ func ParseQuery(apiOp *types.APIRequest, namespaceCache Cache) (sqltypes.ListOpt
}
opts.Pagination = pagination
op := sqltypes.Eq
op := sqltypes.In
projectsOrNamespaces := q.Get(projectsOrNamespacesVar)
if projectsOrNamespaces == "" {
projectsOrNamespaces = q.Get(projectsOrNamespacesVar + notOp)
if projectsOrNamespaces != "" {
op = sqltypes.NotEq
op = sqltypes.NotIn
}
}
if projectsOrNamespaces != "" {
projOrNSFilters, err := parseNamespaceOrProjectFilters(apiOp.Context(), projectsOrNamespaces, op, namespaceCache)
if err != nil {
return opts, err
}
if len(projOrNSFilters) == 0 {
return opts, apierror.NewAPIError(validation.ErrorCode{Code: "No Data", Status: http.StatusNoContent},
fmt.Sprintf("could not find any namespaces named [%s] or namespaces belonging to project named [%s]", projectsOrNamespaces, projectsOrNamespaces))
}
if op == sqltypes.NotEq {
for _, filter := range projOrNSFilters {
opts.Filters = append(opts.Filters, sqltypes.OrFilter{Filters: []sqltypes.Filter{filter}})
}
} else {
opts.Filters = append(opts.Filters, sqltypes.OrFilter{Filters: projOrNSFilters})
}
opts.ProjectsOrNamespaces = parseNamespaceOrProjectFilters(projectsOrNamespaces, op)
}
return opts, nil
@@ -183,40 +166,22 @@ func splitQuery(query string) []string {
return strings.Split(query, ".")
}
func parseNamespaceOrProjectFilters(ctx context.Context, projOrNS string, op sqltypes.Op, namespaceInformer Cache) ([]sqltypes.Filter, error) {
filters := []sqltypes.Filter{}
for _, pn := range strings.Split(projOrNS, ",") {
uList, _, _, err := namespaceInformer.ListByOptions(ctx, &sqltypes.ListOptions{
Filters: []sqltypes.OrFilter{
{
Filters: []sqltypes.Filter{
{
Field: []string{"metadata", "name"},
Matches: []string{pn},
Op: sqltypes.Eq,
},
{
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{pn},
Op: sqltypes.Eq,
},
},
},
},
}, []partition.Partition{{Passthrough: true}}, "")
if err != nil {
return filters, err
}
for _, item := range uList.Items {
filters = append(filters, sqltypes.Filter{
Field: []string{"metadata", "namespace"},
Matches: []string{item.GetName()},
func parseNamespaceOrProjectFilters(projOrNS string, op sqltypes.Op) sqltypes.OrFilter {
var filters []sqltypes.Filter
projOrNs := strings.Split(projOrNS, ",")
if len(projOrNs) > 0 {
filters = []sqltypes.Filter{
sqltypes.Filter{
Field: []string{"metadata", "name"},
Matches: projOrNs,
Op: op,
Partial: false,
})
},
sqltypes.Filter{
Field: []string{"metadata", "labels", projectIDFieldLabel},
Matches: projOrNs,
Op: op,
},
}
continue
}
return filters, nil
return sqltypes.OrFilter{Filters: filters}
}

View File

@@ -1,31 +1,24 @@
package listprocessor
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/sqlcache/partition"
"github.com/rancher/steve/pkg/sqlcache/sqltypes"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
//go:generate mockgen --build_flags=--mod=mod -package listprocessor -destination ./proxy_mocks_test.go github.com/rancher/steve/pkg/stores/sqlproxy Cache
func TestParseQuery(t *testing.T) {
type testCase struct {
description string
setupNSCache func() Cache
nsc Cache
req *types.APIRequest
expectedLO sqltypes.ListOptions
errExpected bool
errorText string
description string
req *types.APIRequest
expectedLO sqltypes.ListOptions
errExpected bool
errorText string
}
var tests []testCase
tests = append(tests, testCase{
@@ -43,116 +36,7 @@ func TestParseQuery(t *testing.T) {
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If projectsornamespaces is not empty" +
" and nsc returns namespaces, they should be included as filters.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "projectsornamespaces=somethin"},
},
},
expectedLO: sqltypes.ListOptions{
Filters: []sqltypes.OrFilter{
{
Filters: []sqltypes.Filter{
{
Field: []string{"metadata", "namespace"},
Matches: []string{"ns1"},
Op: sqltypes.Eq,
Partial: false,
},
},
},
},
Pagination: sqltypes.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
list := &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "ns1",
},
},
},
},
}
nsc := NewMockCache(gomock.NewController(t))
nsc.EXPECT().ListByOptions(context.Background(), &sqltypes.ListOptions{
Filters: []sqltypes.OrFilter{
{
Filters: []sqltypes.Filter{
{
Field: []string{"metadata", "name"},
Matches: []string{"somethin"},
Op: sqltypes.Eq,
},
{
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{"somethin"},
Op: sqltypes.Eq,
},
},
},
},
}, []partition.Partition{{Passthrough: true}}, "").Return(list, len(list.Items), "", nil)
return nsc
},
})
tests = append(tests, testCase{
description: "ParseQuery() with a namespace informer error returned should return an error.",
req: &types.APIRequest{
Request: &http.Request{
// namespace informer is only used if projectsornamespace param is given
URL: &url.URL{RawQuery: "projectsornamespaces=somethin"},
},
},
expectedLO: sqltypes.ListOptions{
Filters: []sqltypes.OrFilter{
{
Filters: []sqltypes.Filter{
{
Field: []string{"metadata", "namespace"},
Matches: []string{"ns1"},
Op: sqltypes.Eq,
Partial: false,
},
},
},
},
Pagination: sqltypes.Pagination{
Page: 1,
},
},
errExpected: true,
setupNSCache: func() Cache {
nsi := NewMockCache(gomock.NewController(t))
nsi.EXPECT().ListByOptions(context.Background(), &sqltypes.ListOptions{
Filters: []sqltypes.OrFilter{
{
Filters: []sqltypes.Filter{
{
Field: []string{"metadata", "name"},
Matches: []string{"somethin"},
Op: sqltypes.Eq,
},
{
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{"somethin"},
Op: sqltypes.Eq,
},
},
},
},
}, []partition.Partition{{Passthrough: true}}, "").Return(nil, 0, "", fmt.Errorf("error"))
return nsi
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If projectsornamespaces is not empty" +
" and nsc does not return namespaces, it should return an empty filter array",
description: "ParseQuery() with only projectsornamespaces should return a project/ns filter.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "projectsornamespaces=somethin"},
@@ -160,36 +44,24 @@ func TestParseQuery(t *testing.T) {
},
expectedLO: sqltypes.ListOptions{
Filters: []sqltypes.OrFilter{},
ProjectsOrNamespaces: sqltypes.OrFilter{
Filters: []sqltypes.Filter{
{
Field: []string{"metadata", "name"},
Matches: []string{"somethin"},
Op: sqltypes.In,
},
{
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{"somethin"},
Op: sqltypes.In,
},
},
},
Pagination: sqltypes.Pagination{
Page: 1,
},
},
errExpected: true,
setupNSCache: func() Cache {
list := &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{},
}
nsi := NewMockCache(gomock.NewController(t))
nsi.EXPECT().ListByOptions(context.Background(), &sqltypes.ListOptions{
Filters: []sqltypes.OrFilter{
{
Filters: []sqltypes.Filter{
{
Field: []string{"metadata", "name"},
Matches: []string{"somethin"},
Op: sqltypes.Eq,
},
{
Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{"somethin"},
Op: sqltypes.Eq,
},
},
},
},
}, []partition.Partition{{Passthrough: true}}, "").Return(list, len(list.Items), "", nil)
return nsi
},
})
tests = append(tests, testCase{
description: "ParseQuery() with filter param set should include filter with partial set to true in list options.",
@@ -848,9 +720,6 @@ func TestParseQuery(t *testing.T) {
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If page param is given, page" +
@@ -886,12 +755,10 @@ func TestParseQuery(t *testing.T) {
t.Parallel()
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
if test.setupNSCache == nil {
test.nsc = nil
} else {
test.nsc = test.setupNSCache()
}
lo, err := ParseQuery(test.req, test.nsc)
//if test.description == "ParseQuery() with no errors: if projectsornamespaces is not empty, it should return an empty filter array" {
// fmt.Println("stop here")
//}
lo, err := ParseQuery(test.req)
if test.errExpected {
assert.NotNil(t, err)
if test.errorText != "" {

View File

@@ -804,7 +804,7 @@ func (s *Store) ListByPartitions(apiOp *types.APIRequest, apiSchema *types.APISc
return nil, 0, "", fmt.Errorf("cachefor %v: %w", gvk, err)
}
opts, err := listprocessor.ParseQuery(apiOp, s.namespaceCache)
opts, err := listprocessor.ParseQuery(apiOp)
if err != nil {
var apiError *apierror.APIError
if errors.As(err, &apiError) {

View File

@@ -256,7 +256,7 @@ func TestListByPartitions(t *testing.T) {
// ListByPartitions copies point so we need some original record of items to ensure as asserting listToReturn's
// items is equal to the list returned by ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems)
opts, err := listprocessor.ParseQuery(req, nil)
opts, err := listprocessor.ParseQuery(req)
assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil)
// This tests that fields are being extracted from schema columns and the type specific fields map
@@ -270,77 +270,6 @@ func TestListByPartitions(t *testing.T) {
assert.Equal(t, "", contToken)
},
})
tests = append(tests, testCase{
description: "client ListByPartitions() with ParseQuery error returned should return an error.",
test: func(t *testing.T) {
nsi := NewMockCache(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
tb := NewMockTransformBuilder(gomock.NewController(t))
ri := NewMockResourceInterface(gomock.NewController(t))
s := &Store{
ctx: context.Background(),
namespaceCache: nsi,
clientGetter: cg,
cacheFactory: cf,
transformBuilder: tb,
}
var partitions []partition.Partition
req := &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "projectsornamespaces=somethin"},
},
}
schema := &types.APISchema{
Schema: &schemas.Schema{Attributes: map[string]interface{}{
"columns": []common.ColumnDefinition{
{
Field: "some.field",
},
},
"verbs": []string{"list", "watch"},
}},
}
expectedItems := []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
},
"data": map[string]interface{}{
"color": "pink",
},
},
},
}
listToReturn := &unstructured.UnstructuredList{
Items: make([]unstructured.Unstructured, len(expectedItems), len(expectedItems)),
}
gvk := schema2.GroupVersionKind{
Group: "some",
Version: "test",
Kind: "gvk",
}
typeSpecificIndexedFields["some_test_gvk"] = [][]string{{"gvk", "specific", "fields"}}
attributes.SetGVK(schema, gvk)
// ListByPartitions copies point so we need some original record of items to ensure as asserting listToReturn's
// items is equal to the list returned by ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems)
nsi.EXPECT().ListByOptions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, 0, "", fmt.Errorf("error")).Times(2)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil)
tb.EXPECT().GetTransformFunc(attributes.GVK(schema), gomock.Any(), false).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
cf.EXPECT().CacheFor(context.Background(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema), true)
_, err := listprocessor.ParseQuery(req, nsi)
assert.NotNil(t, err)
_, _, _, err = s.ListByPartitions(req, schema, partitions)
assert.NotNil(t, err)
},
})
tests = append(tests, testCase{
description: "client ListByPartitions() with no errors returned should return no errors. Should pass fields" +
" from schema.",
@@ -398,9 +327,9 @@ func TestListByPartitions(t *testing.T) {
attributes.SetGVK(schema, gvk)
// ListByPartitions copies point so we need some original record of items to ensure as asserting listToReturn's
// items is equal to the list returned by ListByParititons doesn't ensure no mutation happened
// items is equal to the list returned by ListByPartitions doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems)
_, err := listprocessor.ParseQuery(req, nil)
_, err := listprocessor.ParseQuery(req)
assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(nil, fmt.Errorf("error"))
@@ -474,7 +403,7 @@ func TestListByPartitions(t *testing.T) {
// ListByPartitions copies point so we need some original record of items to ensure as asserting listToReturn's
// items is equal to the list returned by ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems)
opts, err := listprocessor.ParseQuery(req, nil)
opts, err := listprocessor.ParseQuery(req)
assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil)
@@ -550,7 +479,7 @@ func TestListByPartitions(t *testing.T) {
// ListByPartitions copies point so we need some original record of items to ensure as asserting listToReturn's
// items is equal to the list returned by ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems)
_, err := listprocessor.ParseQuery(req, nil)
_, err := listprocessor.ParseQuery(req)
assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil)
// This tests that fields are being extracted from schema columns and the type specific fields map
@@ -627,7 +556,7 @@ func TestListByPartitions(t *testing.T) {
// ListByPartitions copies point so we need some original record of items to ensure as asserting listToReturn's
// items is equal to the list returned by ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems)
opts, err := listprocessor.ParseQuery(req, nil)
opts, err := listprocessor.ParseQuery(req)
assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil)
// This tests that fields are being extracted from schema columns and the type specific fields map