mirror of
https://github.com/rancher/steve.git
synced 2025-09-12 21:39:30 +00:00
* 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:
@@ -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 := ""
|
||||
|
@@ -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 {
|
||||
|
@@ -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.
|
||||
|
@@ -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}
|
||||
}
|
||||
|
@@ -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 != "" {
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user