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

@@ -75,6 +75,8 @@ var (
ErrInvalidColumn = errors.New("supplied column is invalid") ErrInvalidColumn = errors.New("supplied column is invalid")
ErrTooOld = errors.New("resourceversion too old") ErrTooOld = errors.New("resourceversion too old")
projectIDFieldLabel = "field.cattle.io/projectId"
namespacesDbName = "_v1_Namespace"
) )
const ( const (
@@ -632,7 +634,7 @@ type QueryInfo struct {
func (l *ListOptionIndexer) constructQuery(lo *sqltypes.ListOptions, partitions []partition.Partition, namespace string, dbName string) (*QueryInfo, error) { func (l *ListOptionIndexer) constructQuery(lo *sqltypes.ListOptions, partitions []partition.Partition, namespace string, dbName string) (*QueryInfo, error) {
unboundSortLabels := getUnboundSortLabels(lo) unboundSortLabels := getUnboundSortLabels(lo)
queryInfo := &QueryInfo{} queryInfo := &QueryInfo{}
queryUsesLabels := hasLabelFilter(lo.Filters) queryUsesLabels := hasLabelFilter(lo.Filters) || len(lo.ProjectsOrNamespaces.Filters) > 0
joinTableIndexByLabelName := make(map[string]int) joinTableIndexByLabelName := make(map[string]int)
// First, what kind of filtering will we be doing? // 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) // 2- Filtering: WHERE clauses (from lo.Filters)
@@ -696,6 +718,20 @@ func (l *ListOptionIndexer) constructQuery(lo *sqltypes.ListOptions, partitions
params = append(params, orParams...) 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) // WHERE clauses (from namespace)
if namespace != "" && namespace != "*" { if namespace != "" && namespace != "*" {
whereClauses = append(whereClauses, fmt.Sprintf(`f."metadata.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) // 4- Pagination: LIMIT clause (from lo.Pagination)
limitClause := "" limitClause := ""
limit := lo.Pagination.PageSize limit := lo.Pagination.PageSize
if limit > 0 { if limit > 0 {
@@ -982,7 +1017,7 @@ func (l *ListOptionIndexer) buildORClauseFromFilters(orFilters sqltypes.OrFilter
} }
newClause, newParams, err = l.getLabelFilter(index, filter, dbName) newClause, newParams, err = l.getLabelFilter(index, filter, dbName)
} else { } else {
newClause, newParams, err = l.getFieldFilter(filter) newClause, newParams, err = l.getFieldFilter(filter, "f")
} }
if err != nil { if err != nil {
return "", nil, err return "", nil, err
@@ -999,6 +1034,46 @@ func (l *ListOptionIndexer) buildORClauseFromFilters(orFilters sqltypes.OrFilter
return fmt.Sprintf("(%s)", strings.Join(clauses, ") OR (")), params, nil 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) { func buildSortLabelsClause(labelName string, joinTableIndexByLabelName map[string]int, isAsc bool) (string, error) {
ltIndex, err := internLabel(labelName, joinTableIndexByLabelName, -1) ltIndex, err := internLabel(labelName, joinTableIndexByLabelName, -1)
if err != nil { if err != nil {
@@ -1086,10 +1161,10 @@ func internLabel(labelName string, joinTableIndexByLabelName map[string]int, nex
// KEY in VALUES // KEY in VALUES
// KEY notin 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 := "" opString := ""
escapeString := "" escapeString := ""
fieldEntry, err := l.getValidFieldEntry("f", filter.Field) fieldEntry, err := l.getValidFieldEntry(prefix, filter.Field)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
@@ -1146,6 +1221,61 @@ func (l *ListOptionIndexer) getFieldFilter(filter sqltypes.Filter) (string, []an
return "", nil, fmt.Errorf("unrecognized operator: %s", opString) 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) { func (l *ListOptionIndexer) getLabelFilter(index int, filter sqltypes.Filter, dbName string) (string, []any, error) {
opString := "" opString := ""
escapeString := "" escapeString := ""

View File

@@ -1528,6 +1528,138 @@ func TestConstructQuery(t *testing.T) {
expectedStmtArgs: []any{"somevalue"}, expectedStmtArgs: []any{"somevalue"},
expectedErr: nil, 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{ tests = append(tests, testCase{
description: "TestConstructQuery: handles EXISTS statements", description: "TestConstructQuery: handles EXISTS statements",
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{ listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
@@ -2176,7 +2308,7 @@ SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
} }
lii := &ListOptionIndexer{ lii := &ListOptionIndexer{
Indexer: i, 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") queryInfo, err := lii.constructQuery(&test.listOptions, test.partitions, test.ns, "something")
if test.expectedErr != nil { if test.expectedErr != nil {

View File

@@ -28,6 +28,7 @@ const (
// ListOptions represents the query parameters that may be included in a list request. // ListOptions represents the query parameters that may be included in a list request.
type ListOptions struct { type ListOptions struct {
Filters []OrFilter Filters []OrFilter
ProjectsOrNamespaces OrFilter
SortList SortList SortList SortList
Pagination Pagination Pagination Pagination
} }

View File

@@ -4,19 +4,16 @@ package listprocessor
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/rancher/apiserver/pkg/apierror"
"github.com/rancher/apiserver/pkg/types" "github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/sqlcache/partition" "github.com/rancher/steve/pkg/sqlcache/partition"
"github.com/rancher/steve/pkg/sqlcache/sqltypes" "github.com/rancher/steve/pkg/sqlcache/sqltypes"
"github.com/rancher/steve/pkg/stores/queryhelper" "github.com/rancher/steve/pkg/stores/queryhelper"
"github.com/rancher/steve/pkg/stores/sqlpartition/queryparser" "github.com/rancher/steve/pkg/stores/sqlpartition/queryparser"
"github.com/rancher/steve/pkg/stores/sqlpartition/selection" "github.com/rancher/steve/pkg/stores/sqlpartition/selection"
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "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. // 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{} opts := sqltypes.ListOptions{}
q := apiOp.Request.URL.Query() q := apiOp.Request.URL.Query()
@@ -140,30 +137,16 @@ func ParseQuery(apiOp *types.APIRequest, namespaceCache Cache) (sqltypes.ListOpt
} }
opts.Pagination = pagination opts.Pagination = pagination
op := sqltypes.Eq op := sqltypes.In
projectsOrNamespaces := q.Get(projectsOrNamespacesVar) projectsOrNamespaces := q.Get(projectsOrNamespacesVar)
if projectsOrNamespaces == "" { if projectsOrNamespaces == "" {
projectsOrNamespaces = q.Get(projectsOrNamespacesVar + notOp) projectsOrNamespaces = q.Get(projectsOrNamespacesVar + notOp)
if projectsOrNamespaces != "" { if projectsOrNamespaces != "" {
op = sqltypes.NotEq op = sqltypes.NotIn
} }
} }
if projectsOrNamespaces != "" { if projectsOrNamespaces != "" {
projOrNSFilters, err := parseNamespaceOrProjectFilters(apiOp.Context(), projectsOrNamespaces, op, namespaceCache) opts.ProjectsOrNamespaces = parseNamespaceOrProjectFilters(projectsOrNamespaces, op)
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})
}
} }
return opts, nil return opts, nil
@@ -183,40 +166,22 @@ func splitQuery(query string) []string {
return strings.Split(query, ".") return strings.Split(query, ".")
} }
func parseNamespaceOrProjectFilters(ctx context.Context, projOrNS string, op sqltypes.Op, namespaceInformer Cache) ([]sqltypes.Filter, error) { func parseNamespaceOrProjectFilters(projOrNS string, op sqltypes.Op) sqltypes.OrFilter {
filters := []sqltypes.Filter{} var filters []sqltypes.Filter
for _, pn := range strings.Split(projOrNS, ",") { projOrNs := strings.Split(projOrNS, ",")
uList, _, _, err := namespaceInformer.ListByOptions(ctx, &sqltypes.ListOptions{ if len(projOrNs) > 0 {
Filters: []sqltypes.OrFilter{ filters = []sqltypes.Filter{
{ sqltypes.Filter{
Filters: []sqltypes.Filter{
{
Field: []string{"metadata", "name"}, Field: []string{"metadata", "name"},
Matches: []string{pn}, Matches: projOrNs,
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()},
Op: op, Op: op,
Partial: false, },
}) sqltypes.Filter{
Field: []string{"metadata", "labels", projectIDFieldLabel},
Matches: projOrNs,
Op: op,
},
} }
continue
} }
return sqltypes.OrFilter{Filters: filters}
return filters, nil
} }

View File

@@ -1,18 +1,13 @@
package listprocessor package listprocessor
import ( import (
"context"
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"testing" "testing"
"github.com/rancher/apiserver/pkg/types" "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/sqlcache/sqltypes"
"github.com/stretchr/testify/assert" "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 //go:generate mockgen --build_flags=--mod=mod -package listprocessor -destination ./proxy_mocks_test.go github.com/rancher/steve/pkg/stores/sqlproxy Cache
@@ -20,8 +15,6 @@ import (
func TestParseQuery(t *testing.T) { func TestParseQuery(t *testing.T) {
type testCase struct { type testCase struct {
description string description string
setupNSCache func() Cache
nsc Cache
req *types.APIRequest req *types.APIRequest
expectedLO sqltypes.ListOptions expectedLO sqltypes.ListOptions
errExpected bool errExpected bool
@@ -43,116 +36,7 @@ func TestParseQuery(t *testing.T) {
}, },
}) })
tests = append(tests, testCase{ tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If projectsornamespaces is not empty" + description: "ParseQuery() with only projectsornamespaces should return a project/ns filter.",
" 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",
req: &types.APIRequest{ req: &types.APIRequest{
Request: &http.Request{ Request: &http.Request{
URL: &url.URL{RawQuery: "projectsornamespaces=somethin"}, URL: &url.URL{RawQuery: "projectsornamespaces=somethin"},
@@ -160,35 +44,23 @@ func TestParseQuery(t *testing.T) {
}, },
expectedLO: sqltypes.ListOptions{ expectedLO: sqltypes.ListOptions{
Filters: []sqltypes.OrFilter{}, Filters: []sqltypes.OrFilter{},
Pagination: sqltypes.Pagination{ ProjectsOrNamespaces: sqltypes.OrFilter{
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{ Filters: []sqltypes.Filter{
{ {
Field: []string{"metadata", "name"}, Field: []string{"metadata", "name"},
Matches: []string{"somethin"}, Matches: []string{"somethin"},
Op: sqltypes.Eq, Op: sqltypes.In,
}, },
{ {
Field: []string{"metadata", "labels", "field.cattle.io/projectId"}, Field: []string{"metadata", "labels", "field.cattle.io/projectId"},
Matches: []string{"somethin"}, Matches: []string{"somethin"},
Op: sqltypes.Eq, Op: sqltypes.In,
}, },
}, },
}, },
Pagination: sqltypes.Pagination{
Page: 1,
}, },
}, []partition.Partition{{Passthrough: true}}, "").Return(list, len(list.Items), "", nil)
return nsi
}, },
}) })
tests = append(tests, testCase{ tests = append(tests, testCase{
@@ -848,9 +720,6 @@ func TestParseQuery(t *testing.T) {
Page: 1, Page: 1,
}, },
}, },
setupNSCache: func() Cache {
return nil
},
}) })
tests = append(tests, testCase{ tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If page param is given, page" + 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() t.Parallel()
for _, test := range tests { for _, test := range tests {
t.Run(test.description, func(t *testing.T) { t.Run(test.description, func(t *testing.T) {
if test.setupNSCache == nil { //if test.description == "ParseQuery() with no errors: if projectsornamespaces is not empty, it should return an empty filter array" {
test.nsc = nil // fmt.Println("stop here")
} else { //}
test.nsc = test.setupNSCache() lo, err := ParseQuery(test.req)
}
lo, err := ParseQuery(test.req, test.nsc)
if test.errExpected { if test.errExpected {
assert.NotNil(t, err) assert.NotNil(t, err)
if test.errorText != "" { 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) 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 { if err != nil {
var apiError *apierror.APIError var apiError *apierror.APIError
if errors.As(err, &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 // 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 ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems) copy(listToReturn.Items, expectedItems)
opts, err := listprocessor.ParseQuery(req, nil) opts, err := listprocessor.ParseQuery(req)
assert.Nil(t, err) assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil) 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 // 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) 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{ tests = append(tests, testCase{
description: "client ListByPartitions() with no errors returned should return no errors. Should pass fields" + description: "client ListByPartitions() with no errors returned should return no errors. Should pass fields" +
" from schema.", " from schema.",
@@ -398,9 +327,9 @@ func TestListByPartitions(t *testing.T) {
attributes.SetGVK(schema, gvk) attributes.SetGVK(schema, gvk)
// ListByPartitions copies point so we need some original record of items to ensure as asserting listToReturn's // 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) copy(listToReturn.Items, expectedItems)
_, err := listprocessor.ParseQuery(req, nil) _, err := listprocessor.ParseQuery(req)
assert.Nil(t, err) assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(nil, fmt.Errorf("error")) 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 // 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 ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems) copy(listToReturn.Items, expectedItems)
opts, err := listprocessor.ParseQuery(req, nil) opts, err := listprocessor.ParseQuery(req)
assert.Nil(t, err) assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil) 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 // 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 ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems) copy(listToReturn.Items, expectedItems)
_, err := listprocessor.ParseQuery(req, nil) _, err := listprocessor.ParseQuery(req)
assert.Nil(t, err) assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil) 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 // 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 // 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 ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems) copy(listToReturn.Items, expectedItems)
opts, err := listprocessor.ParseQuery(req, nil) opts, err := listprocessor.ParseQuery(req)
assert.Nil(t, err) assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil) 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 // This tests that fields are being extracted from schema columns and the type specific fields map