1
0
mirror of https://github.com/rancher/steve.git synced 2025-09-18 08:20:36 +00:00

SQLite backed cache (#223)

This uses SQLite-backed informers provided by Lasso with https://github.com/rancher/lasso/pull/65 to implement Steve API (/v1/) functionality.

This new functionality is available behind a feature flag to be specified at Steve startup

See https://confluence.suse.com/pages/viewpage.action?pageId=1359086083 

Co-authored-by: Ricardo Weir <ricardo.weir@suse.com>
Co-authored-by: Michael Bolot <michael.bolot@suse.com>
Co-authored-by: Silvio Moioli <silvio@moioli.net>
Signed-off-by: Silvio Moioli <silvio@moioli.net>
This commit is contained in:
Silvio Moioli
2024-06-05 16:17:12 +02:00
committed by GitHub
parent 4cf4e6b385
commit 7a84620e8b
30 changed files with 4826 additions and 46 deletions

View File

@@ -0,0 +1,196 @@
// Package listprocessor contains methods for filtering, sorting, and paginating lists of objects.
package listprocessor
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/rancher/apiserver/pkg/apierror"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/lasso/pkg/cache/sql/informer"
"github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/rancher/wrangler/v2/pkg/schemas/validation"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
const (
defaultLimit = 100000
continueParam = "continue"
limitParam = "limit"
filterParam = "filter"
sortParam = "sort"
pageSizeParam = "pagesize"
pageParam = "page"
revisionParam = "revision"
projectsOrNamespacesVar = "projectsornamespaces"
projectIDFieldLabel = "field.cattle.io/projectId"
orOp = ","
notOp = "!"
)
var opReg = regexp.MustCompile(`[!]?=`)
// ListOptions represents the query parameters that may be included in a list request.
type ListOptions struct {
ChunkSize int
Resume string
Filters []informer.OrFilter
Sort informer.Sort
Pagination informer.Pagination
}
type Cache interface {
// ListByOptions returns objects according to the specified list options and partitions
ListByOptions(ctx context.Context, lo informer.ListOptions, partitions []partition.Partition, namespace string) (*unstructured.UnstructuredList, string, error)
}
// ParseQuery parses the query params of a request and returns a ListOptions.
func ParseQuery(apiOp *types.APIRequest, namespaceCache Cache) (informer.ListOptions, error) {
opts := informer.ListOptions{}
opts.ChunkSize = getLimit(apiOp)
q := apiOp.Request.URL.Query()
cont := q.Get(continueParam)
opts.Resume = cont
filterParams := q[filterParam]
filterOpts := []informer.OrFilter{}
for _, filters := range filterParams {
orFilters := strings.Split(filters, orOp)
orFilter := informer.OrFilter{}
for _, filter := range orFilters {
var op informer.Op
if strings.Contains(filter, "!=") {
op = "!="
}
filter := opReg.Split(filter, -1)
if len(filter) != 2 {
continue
}
usePartialMatch := !(strings.HasPrefix(filter[1], `'`) && strings.HasSuffix(filter[1], `'`))
value := strings.TrimSuffix(strings.TrimPrefix(filter[1], "'"), "'")
orFilter.Filters = append(orFilter.Filters, informer.Filter{Field: strings.Split(filter[0], "."), Match: value, Op: op, Partial: usePartialMatch})
}
filterOpts = append(filterOpts, orFilter)
}
opts.Filters = filterOpts
sortOpts := informer.Sort{}
sortKeys := q.Get(sortParam)
if sortKeys != "" {
sortParts := strings.SplitN(sortKeys, ",", 2)
primaryField := sortParts[0]
if primaryField != "" && primaryField[0] == '-' {
sortOpts.PrimaryOrder = informer.DESC
primaryField = primaryField[1:]
}
if primaryField != "" {
sortOpts.PrimaryField = strings.Split(primaryField, ".")
}
if len(sortParts) > 1 {
secondaryField := sortParts[1]
if secondaryField != "" && secondaryField[0] == '-' {
sortOpts.SecondaryOrder = informer.DESC
secondaryField = secondaryField[1:]
}
if secondaryField != "" {
sortOpts.SecondaryField = strings.Split(secondaryField, ".")
}
}
}
opts.Sort = sortOpts
var err error
pagination := informer.Pagination{}
pagination.PageSize, err = strconv.Atoi(q.Get(pageSizeParam))
if err != nil {
pagination.PageSize = 0
}
pagination.Page, err = strconv.Atoi(q.Get(pageParam))
if err != nil {
pagination.Page = 1
}
opts.Pagination = pagination
var op informer.Op
projectsOrNamespaces := q.Get(projectsOrNamespacesVar)
if projectsOrNamespaces == "" {
projectsOrNamespaces = q.Get(projectsOrNamespacesVar + notOp)
if projectsOrNamespaces != "" {
op = informer.NotEq
}
}
if projectsOrNamespaces != "" {
projOrNSFilters, err := parseNamespaceOrProjectFilters(apiOp.Context(), projectsOrNamespaces, op, namespaceCache)
if err != nil {
return opts, err
}
if projOrNSFilters == nil {
return opts, apierror.NewAPIError(validation.NotFound, fmt.Sprintf("could not find any namespacess named [%s] or namespaces belonging to project named [%s]", projectsOrNamespaces, projectsOrNamespaces))
}
if op == informer.NotEq {
for _, filter := range projOrNSFilters {
opts.Filters = append(opts.Filters, informer.OrFilter{Filters: []informer.Filter{filter}})
}
} else {
opts.Filters = append(opts.Filters, informer.OrFilter{Filters: projOrNSFilters})
}
}
return opts, nil
}
// getLimit extracts the limit parameter from the request or sets a default of 100000.
// The default limit can be explicitly disabled by setting it to zero or negative.
// If the default is accepted, clients must be aware that the list may be incomplete, and use the "continue" token to get the next chunk of results.
func getLimit(apiOp *types.APIRequest) int {
limitString := apiOp.Request.URL.Query().Get(limitParam)
limit, err := strconv.Atoi(limitString)
if err != nil {
limit = defaultLimit
}
return limit
}
func parseNamespaceOrProjectFilters(ctx context.Context, projOrNS string, op informer.Op, namespaceInformer Cache) ([]informer.Filter, error) {
var filters []informer.Filter
for _, pn := range strings.Split(projOrNS, ",") {
uList, _, err := namespaceInformer.ListByOptions(ctx, informer.ListOptions{
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "name"},
Match: pn,
Op: informer.Eq,
},
{
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
Match: pn,
Op: informer.Eq,
},
},
},
},
}, []partition.Partition{{Passthrough: true}}, "")
if err != nil {
return filters, err
}
for _, item := range uList.Items {
filters = append(filters, informer.Filter{
Field: []string{"metadata", "namespace"},
Match: item.GetName(),
Op: op,
Partial: false,
})
}
continue
}
return filters, nil
}

View File

@@ -0,0 +1,524 @@
package listprocessor
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/golang/mock/gomock"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/lasso/pkg/cache/sql/informer"
"github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/stretchr/testify/assert"
"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 informer.ListOptions
errExpected bool
}
var tests []testCase
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. Should have proper defaults set.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: ""},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
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: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "namespace"},
Match: "ns1",
Op: "",
Partial: false,
},
},
},
},
Pagination: informer.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(), informer.ListOptions{
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "name"},
Match: "somethin",
Op: informer.Eq,
},
{
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
Match: "somethin",
Op: informer.Eq,
},
},
},
},
}, []partition.Partition{{Passthrough: true}}, "").Return(list, "", 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: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "namespace"},
Match: "ns1",
Op: "",
Partial: false,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
errExpected: true,
setupNSCache: func() Cache {
nsi := NewMockCache(gomock.NewController(t))
nsi.EXPECT().ListByOptions(context.Background(), informer.ListOptions{
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "name"},
Match: "somethin",
Op: informer.Eq,
},
{
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
Match: "somethin",
Op: informer.Eq,
},
},
},
},
}, []partition.Partition{{Passthrough: true}}, "").Return(nil, "", 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, an error should be returned.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "projectsornamespaces=somethin"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "namespace"},
Match: "ns1",
Op: "",
Partial: false,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
errExpected: true,
setupNSCache: func() Cache {
list := &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{},
}
nsi := NewMockCache(gomock.NewController(t))
nsi.EXPECT().ListByOptions(context.Background(), informer.ListOptions{
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "name"},
Match: "somethin",
Op: informer.Eq,
},
{
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
Match: "somethin",
Op: informer.Eq,
},
},
},
},
}, []partition.Partition{{Passthrough: true}}, "").Return(list, "", nil)
return nsi
},
})
tests = append(tests, testCase{
description: "ParseQuery() with filter param set should include filter with partial set to true in list options.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "filter=a=c"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"a"},
Match: "c",
Op: "",
Partial: true,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with filter param set, with value in single quotes, should include filter with partial set to false in list options.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "filter=a='c'"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"a"},
Match: "c",
Op: "",
Partial: false,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with multiple filter params, should include multiple or filters.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "filter=a=c&filter=b=d"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"a"},
Match: "c",
Op: "",
Partial: true,
},
},
},
{
Filters: []informer.Filter{
{
Field: []string{"b"},
Match: "d",
Op: "",
Partial: true,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with a filter param with a comma separate value, should include a single or filter with" +
" multiple filters.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "filter=a=c,b=d"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"a"},
Match: "c",
Op: "",
Partial: true,
},
{
Field: []string{"b"},
Match: "d",
Op: "",
Partial: true,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If one sort param is given, primary field" +
" sort option should be set",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "sort=metadata.name"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Sort: informer.Sort{
PrimaryField: []string{"metadata", "name"},
},
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If one sort param is given primary field " +
"and hyphen prefix for field value, sort option should be set with descending order.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "sort=-metadata.name"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Sort: informer.Sort{
PrimaryField: []string{"metadata", "name"},
PrimaryOrder: informer.DESC,
},
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If two sort params are given, sort " +
"options with primary field and secondary field should be set.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "sort=-metadata.name,spec.something"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Sort: informer.Sort{
PrimaryField: []string{"metadata", "name"},
PrimaryOrder: informer.DESC,
SecondaryField: []string{"spec", "something"},
SecondaryOrder: informer.ASC,
},
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If continue params is given, resume" +
" should be set with assigned value.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "continue=5"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Resume: "5",
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If continue param is given, resume" +
" should be set with assigned value.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "continue=5"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Resume: "5",
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If limit param is given, chunksize" +
" should be set with assigned value.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "limit=3"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: 3,
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
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" +
" should be set with assigned value.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "page=3"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 3,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If pagesize param is given, pageSize" +
" should be set with assigned value.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "pagesize=20"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
PageSize: 20,
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
t.Parallel()
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
test.nsc = test.setupNSCache()
lo, err := ParseQuery(test.req, test.nsc)
if test.errExpected {
assert.NotNil(t, err)
return
}
assert.Equal(t, test.expectedLO, lo)
})
}
}

View File

@@ -0,0 +1,54 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/rancher/steve/pkg/stores/sqlproxy (interfaces: Cache)
// Package listprocessor is a generated GoMock package.
package listprocessor
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
informer "github.com/rancher/lasso/pkg/cache/sql/informer"
partition "github.com/rancher/lasso/pkg/cache/sql/partition"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// MockCache is a mock of Cache interface.
type MockCache struct {
ctrl *gomock.Controller
recorder *MockCacheMockRecorder
}
// MockCacheMockRecorder is the mock recorder for MockCache.
type MockCacheMockRecorder struct {
mock *MockCache
}
// NewMockCache creates a new mock instance.
func NewMockCache(ctrl *gomock.Controller) *MockCache {
mock := &MockCache{ctrl: ctrl}
mock.recorder = &MockCacheMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCache) EXPECT() *MockCacheMockRecorder {
return m.recorder
}
// ListByOptions mocks base method.
func (m *MockCache) ListByOptions(arg0 context.Context, arg1 informer.ListOptions, arg2 []partition.Partition, arg3 string) (*unstructured.UnstructuredList, string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListByOptions", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(*unstructured.UnstructuredList)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// ListByOptions indicates an expected call of ListByOptions.
func (mr *MockCacheMockRecorder) ListByOptions(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByOptions", reflect.TypeOf((*MockCache)(nil).ListByOptions), arg0, arg1, arg2, arg3)
}

View File

@@ -0,0 +1,185 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/rancher/steve/pkg/stores/sqlpartition (interfaces: Partitioner,UnstructuredStore)
// Package sqlpartition is a generated GoMock package.
package sqlpartition
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
types "github.com/rancher/apiserver/pkg/types"
partition "github.com/rancher/lasso/pkg/cache/sql/partition"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
watch "k8s.io/apimachinery/pkg/watch"
)
// MockPartitioner is a mock of Partitioner interface.
type MockPartitioner struct {
ctrl *gomock.Controller
recorder *MockPartitionerMockRecorder
}
// MockPartitionerMockRecorder is the mock recorder for MockPartitioner.
type MockPartitionerMockRecorder struct {
mock *MockPartitioner
}
// NewMockPartitioner creates a new mock instance.
func NewMockPartitioner(ctrl *gomock.Controller) *MockPartitioner {
mock := &MockPartitioner{ctrl: ctrl}
mock.recorder = &MockPartitionerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPartitioner) EXPECT() *MockPartitionerMockRecorder {
return m.recorder
}
// All mocks base method.
func (m *MockPartitioner) All(arg0 *types.APIRequest, arg1 *types.APISchema, arg2, arg3 string) ([]partition.Partition, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "All", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].([]partition.Partition)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// All indicates an expected call of All.
func (mr *MockPartitionerMockRecorder) All(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "All", reflect.TypeOf((*MockPartitioner)(nil).All), arg0, arg1, arg2, arg3)
}
// Store mocks base method.
func (m *MockPartitioner) Store() UnstructuredStore {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Store")
ret0, _ := ret[0].(UnstructuredStore)
return ret0
}
// Store indicates an expected call of Store.
func (mr *MockPartitionerMockRecorder) Store() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Store", reflect.TypeOf((*MockPartitioner)(nil).Store))
}
// MockUnstructuredStore is a mock of UnstructuredStore interface.
type MockUnstructuredStore struct {
ctrl *gomock.Controller
recorder *MockUnstructuredStoreMockRecorder
}
// MockUnstructuredStoreMockRecorder is the mock recorder for MockUnstructuredStore.
type MockUnstructuredStoreMockRecorder struct {
mock *MockUnstructuredStore
}
// NewMockUnstructuredStore creates a new mock instance.
func NewMockUnstructuredStore(ctrl *gomock.Controller) *MockUnstructuredStore {
mock := &MockUnstructuredStore{ctrl: ctrl}
mock.recorder = &MockUnstructuredStoreMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUnstructuredStore) EXPECT() *MockUnstructuredStoreMockRecorder {
return m.recorder
}
// ByID mocks base method.
func (m *MockUnstructuredStore) ByID(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 string) (*unstructured.Unstructured, []types.Warning, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ByID", arg0, arg1, arg2)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].([]types.Warning)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// ByID indicates an expected call of ByID.
func (mr *MockUnstructuredStoreMockRecorder) ByID(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ByID", reflect.TypeOf((*MockUnstructuredStore)(nil).ByID), arg0, arg1, arg2)
}
// Create mocks base method.
func (m *MockUnstructuredStore) Create(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 types.APIObject) (*unstructured.Unstructured, []types.Warning, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", arg0, arg1, arg2)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].([]types.Warning)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Create indicates an expected call of Create.
func (mr *MockUnstructuredStoreMockRecorder) Create(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUnstructuredStore)(nil).Create), arg0, arg1, arg2)
}
// Delete mocks base method.
func (m *MockUnstructuredStore) Delete(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 string) (*unstructured.Unstructured, []types.Warning, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].([]types.Warning)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Delete indicates an expected call of Delete.
func (mr *MockUnstructuredStoreMockRecorder) Delete(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUnstructuredStore)(nil).Delete), arg0, arg1, arg2)
}
// ListByPartitions mocks base method.
func (m *MockUnstructuredStore) ListByPartitions(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 []partition.Partition) ([]unstructured.Unstructured, string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListByPartitions", arg0, arg1, arg2)
ret0, _ := ret[0].([]unstructured.Unstructured)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// ListByPartitions indicates an expected call of ListByPartitions.
func (mr *MockUnstructuredStoreMockRecorder) ListByPartitions(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByPartitions", reflect.TypeOf((*MockUnstructuredStore)(nil).ListByPartitions), arg0, arg1, arg2)
}
// Update mocks base method.
func (m *MockUnstructuredStore) Update(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 types.APIObject, arg3 string) (*unstructured.Unstructured, []types.Warning, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].([]types.Warning)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Update indicates an expected call of Update.
func (mr *MockUnstructuredStoreMockRecorder) Update(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUnstructuredStore)(nil).Update), arg0, arg1, arg2, arg3)
}
// WatchByPartitions mocks base method.
func (m *MockUnstructuredStore) WatchByPartitions(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 types.WatchRequest, arg3 []partition.Partition) (chan watch.Event, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WatchByPartitions", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(chan watch.Event)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// WatchByPartitions indicates an expected call of WatchByPartitions.
func (mr *MockUnstructuredStoreMockRecorder) WatchByPartitions(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WatchByPartitions", reflect.TypeOf((*MockUnstructuredStore)(nil).WatchByPartitions), arg0, arg1, arg2, arg3)
}

View File

@@ -0,0 +1,118 @@
package sqlpartition
import (
"fmt"
"sort"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/attributes"
"github.com/rancher/wrangler/v2/pkg/kv"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/watch"
)
var (
passthroughPartitions = []partition.Partition{
{Passthrough: true},
}
)
// UnstructuredStore is like types.Store but deals in k8s unstructured objects instead of apiserver types.
// This interface exists in order for store to be mocked in tests
type UnstructuredStore interface {
ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, []types.Warning, error)
Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (*unstructured.Unstructured, []types.Warning, error)
Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (*unstructured.Unstructured, []types.Warning, error)
Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, []types.Warning, error)
ListByPartitions(apiOp *types.APIRequest, schema *types.APISchema, partitions []partition.Partition) ([]unstructured.Unstructured, string, error)
WatchByPartitions(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest, partitions []partition.Partition) (chan watch.Event, error)
}
// rbacPartitioner is an implementation of the sqlpartition.Partitioner interface.
type rbacPartitioner struct {
proxyStore UnstructuredStore
}
// All returns a slice of partitions applicable to the API schema and the user's access level.
// For watching individual resources or for blanket access permissions, it returns the passthrough partition.
// For more granular permissions, it returns a slice of partitions matching an allowed namespace or resource names.
func (p *rbacPartitioner) All(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) ([]partition.Partition, error) {
switch verb {
case "list":
fallthrough
case "watch":
if id != "" {
ns, name := kv.RSplit(id, "/")
return []partition.Partition{
{
Namespace: ns,
All: false,
Passthrough: false,
Names: sets.New[string](name),
},
}, nil
}
partitions, passthrough := isPassthrough(apiOp, schema, verb)
if passthrough {
return passthroughPartitions, nil
}
sort.Slice(partitions, func(i, j int) bool {
return partitions[i].Namespace < partitions[j].Namespace
})
return partitions, nil
default:
return nil, fmt.Errorf("parition all: invalid verb %s", verb)
}
}
// Store returns an Store suited to listing and watching resources by partition.
func (p *rbacPartitioner) Store() UnstructuredStore {
return p.proxyStore
}
// isPassthrough determines whether a request can be passed through directly to the underlying store
// or if the results need to be partitioned by namespace and name based on the requester's access.
func isPassthrough(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]partition.Partition, bool) {
accessListByVerb, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
if accessListByVerb.All(verb) {
return nil, true
}
resources := accessListByVerb.Granted(verb)
if apiOp.Namespace != "" {
if resources[apiOp.Namespace].All {
return nil, true
}
return []partition.Partition{
{
Namespace: apiOp.Namespace,
Names: sets.Set[string](resources[apiOp.Namespace].Names),
},
}, false
}
var result []partition.Partition
if attributes.Namespaced(schema) {
for k, v := range resources {
result = append(result, partition.Partition{
Namespace: k,
All: v.All,
Names: sets.Set[string](v.Names),
})
}
} else {
for _, v := range resources {
result = append(result, partition.Partition{
All: v.All,
Names: sets.Set[string](v.Names),
})
}
}
return result, false
}

View File

@@ -0,0 +1,263 @@
package sqlpartition
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/wrangler/v2/pkg/schemas"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/util/sets"
)
func TestAll(t *testing.T) {
tests := []struct {
name string
apiOp *types.APIRequest
id string
schema *types.APISchema
wantPartitions []partition.Partition
}{
{
name: "all passthrough",
apiOp: &types.APIRequest{},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "foo",
Attributes: map[string]interface{}{
"access": accesscontrol.AccessListByVerb{
"list": accesscontrol.AccessList{
accesscontrol.Access{
Namespace: "*",
ResourceName: "*",
},
},
},
},
},
},
wantPartitions: passthroughPartitions,
},
{
name: "global access for global request",
apiOp: &types.APIRequest{},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "foo",
Attributes: map[string]interface{}{
"access": accesscontrol.AccessListByVerb{
"list": accesscontrol.AccessList{
accesscontrol.Access{
Namespace: "*",
ResourceName: "r1",
},
accesscontrol.Access{
Namespace: "*",
ResourceName: "r2",
},
},
},
},
},
},
wantPartitions: []partition.Partition{
{
Names: sets.New[string]("r1", "r2"),
},
},
},
{
name: "namespace access for global request",
apiOp: &types.APIRequest{},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "foo",
Attributes: map[string]interface{}{
"namespaced": true,
"access": accesscontrol.AccessListByVerb{
"list": accesscontrol.AccessList{
accesscontrol.Access{
Namespace: "n1",
ResourceName: "*",
},
accesscontrol.Access{
Namespace: "n2",
ResourceName: "*",
},
},
},
},
},
},
wantPartitions: []partition.Partition{
{
Namespace: "n1",
All: true,
},
{
Namespace: "n2",
All: true,
},
},
},
{
name: "namespace access for namespaced request",
apiOp: &types.APIRequest{
Namespace: "n1",
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "foo",
Attributes: map[string]interface{}{
"namespaced": true,
"access": accesscontrol.AccessListByVerb{
"list": accesscontrol.AccessList{
accesscontrol.Access{
Namespace: "n1",
ResourceName: "*",
},
},
},
},
},
},
wantPartitions: passthroughPartitions,
},
{
// we still get a partition even if there is no access to it, it will be rejected by the API server later
name: "namespace access for invalid namespaced request",
apiOp: &types.APIRequest{
Namespace: "n2",
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "foo",
Attributes: map[string]interface{}{
"namespaced": true,
"access": accesscontrol.AccessListByVerb{
"list": accesscontrol.AccessList{
accesscontrol.Access{
Namespace: "n1",
ResourceName: "*",
},
},
},
},
},
},
wantPartitions: []partition.Partition{
{
Namespace: "n2",
},
},
},
{
name: "by names access for global request",
apiOp: &types.APIRequest{},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "foo",
Attributes: map[string]interface{}{
"namespaced": true,
"access": accesscontrol.AccessListByVerb{
"list": accesscontrol.AccessList{
accesscontrol.Access{
Namespace: "n1",
ResourceName: "r1",
},
accesscontrol.Access{
Namespace: "n1",
ResourceName: "r2",
},
accesscontrol.Access{
Namespace: "n2",
ResourceName: "r1",
},
},
},
},
},
},
wantPartitions: []partition.Partition{
{
Namespace: "n1",
Names: sets.New[string]("r1", "r2"),
},
{
Namespace: "n2",
Names: sets.New[string]("r1"),
},
},
},
{
name: "by names access for namespaced request",
apiOp: &types.APIRequest{
Namespace: "n1",
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "foo",
Attributes: map[string]interface{}{
"namespaced": true,
"access": accesscontrol.AccessListByVerb{
"list": accesscontrol.AccessList{
accesscontrol.Access{
Namespace: "n1",
ResourceName: "r1",
},
accesscontrol.Access{
Namespace: "n2",
ResourceName: "r1",
},
},
},
},
},
},
wantPartitions: []partition.Partition{
{
Namespace: "n1",
Names: sets.New[string]("r1"),
},
},
},
{
name: "by id",
apiOp: &types.APIRequest{},
id: "n1/r1",
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "foo",
},
},
wantPartitions: []partition.Partition{
{
Namespace: "n1",
Names: sets.New[string]("r1"),
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
partitioner := rbacPartitioner{}
verb := "list"
gotPartitions, gotErr := partitioner.All(test.apiOp, test.schema, verb, test.id)
assert.Nil(t, gotErr)
assert.Equal(t, test.wantPartitions, gotPartitions)
})
}
}
func TestStore(t *testing.T) {
expectedStore := NewMockUnstructuredStore(gomock.NewController(t))
rp := rbacPartitioner{
proxyStore: expectedStore,
}
store := rp.Store()
assert.Equal(t, expectedStore, store)
}

View File

@@ -0,0 +1,142 @@
// Package sqlpartition implements a store which converts a request to partitions based on the user's rbac for
// the resource. For example, a user may request all items of resource A, but only have permissions for resource A in
// namespaces x,y,z. The partitions will then store that information and be passed to the next store.
package sqlpartition
import (
"context"
"github.com/rancher/apiserver/pkg/types"
lassopartition "github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/stores/partition"
)
// Partitioner is an interface for interacting with partitions.
type Partitioner interface {
All(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) ([]lassopartition.Partition, error)
Store() UnstructuredStore
}
type SchemaColumnSetter interface {
SetColumns(ctx context.Context, schema *types.APISchema) error
}
// Store implements types.proxyStore for partitions.
type Store struct {
Partitioner Partitioner
asl accesscontrol.AccessSetLookup
}
// NewStore creates a types.proxyStore implementation with a partitioner
func NewStore(store UnstructuredStore, asl accesscontrol.AccessSetLookup) *Store {
s := &Store{
Partitioner: &rbacPartitioner{
proxyStore: store,
},
asl: asl,
}
return s
}
// Delete deletes an object from a store.
func (s *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
target := s.Partitioner.Store()
obj, warnings, err := target.Delete(apiOp, schema, id)
if err != nil {
return types.APIObject{}, err
}
return partition.ToAPI(schema, obj, warnings), nil
}
// ByID looks up a single object by its ID.
func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
target := s.Partitioner.Store()
obj, warnings, err := target.ByID(apiOp, schema, id)
if err != nil {
return types.APIObject{}, err
}
return partition.ToAPI(schema, obj, warnings), nil
}
// List returns a list of objects across all applicable partitions.
// If pagination parameters are used, it returns a segment of the list.
func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
var (
result types.APIObjectList
)
partitions, err := s.Partitioner.All(apiOp, schema, "list", "")
if err != nil {
return result, err
}
store := s.Partitioner.Store()
list, continueToken, err := store.ListByPartitions(apiOp, schema, partitions)
if err != nil {
return result, err
}
result.Count = len(list)
for _, item := range list {
item := item.DeepCopy()
result.Objects = append(result.Objects, partition.ToAPI(schema, item, nil))
}
result.Revision = ""
result.Continue = continueToken
return result, nil
}
// Create creates a single object in the store.
func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (types.APIObject, error) {
target := s.Partitioner.Store()
obj, warnings, err := target.Create(apiOp, schema, data)
if err != nil {
return types.APIObject{}, err
}
return partition.ToAPI(schema, obj, warnings), nil
}
// Update updates a single object in the store.
func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
target := s.Partitioner.Store()
obj, warnings, err := target.Update(apiOp, schema, data, id)
if err != nil {
return types.APIObject{}, err
}
return partition.ToAPI(schema, obj, warnings), nil
}
// Watch returns a channel of events for a list or resource.
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
partitions, err := s.Partitioner.All(apiOp, schema, "watch", wr.ID)
if err != nil {
return nil, err
}
store := s.Partitioner.Store()
response := make(chan types.APIEvent)
c, err := store.WatchByPartitions(apiOp, schema, wr, partitions)
if err != nil {
return nil, err
}
go func() {
defer close(response)
for i := range c {
response <- partition.ToAPIEvent(nil, schema, i)
}
}()
return response, nil
}

View File

@@ -0,0 +1,383 @@
package sqlpartition
import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strconv"
"testing"
"github.com/golang/mock/gomock"
"github.com/rancher/wrangler/v2/pkg/schemas"
"github.com/stretchr/testify/assert"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/stores/sqlproxy"
"github.com/rancher/wrangler/v2/pkg/generic"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request"
)
//go:generate mockgen --build_flags=--mod=mod -package sqlpartition -destination partition_mocks_test.go "github.com/rancher/steve/pkg/stores/sqlpartition" Partitioner,UnstructuredStore
func TestList(t *testing.T) {
type testCase struct {
description string
test func(t *testing.T)
}
var tests []testCase
tests = append(tests, testCase{
description: "List() with no errors returned should returned no errors. Should have empty reivsion, count " +
"should match number of items in list, and id should include namespace (if applicable) and name, separated" +
" by a '/'.",
test: func(t *testing.T) {
p := NewMockPartitioner(gomock.NewController(t))
us := NewMockUnstructuredStore(gomock.NewController(t))
s := Store{
Partitioner: p,
}
req := &types.APIRequest{}
schema := &types.APISchema{
Schema: &schemas.Schema{},
}
partitions := make([]partition.Partition, 0)
uListToReturn := []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "fruitsnamespace",
},
"data": map[string]interface{}{
"color": "pink",
},
},
},
}
expectedAPIObjList := types.APIObjectList{
Count: 1,
Revision: "",
Objects: []types.APIObject{
{
Object: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "fruitsnamespace",
},
"data": map[string]interface{}{
"color": "pink",
},
},
},
ID: "fruitsnamespace/fuji",
},
},
}
p.EXPECT().All(req, schema, "list", "").Return(partitions, nil)
p.EXPECT().Store().Return(us)
us.EXPECT().ListByPartitions(req, schema, partitions).Return(uListToReturn, "", nil)
l, err := s.List(req, schema)
assert.Nil(t, err)
assert.Equal(t, expectedAPIObjList, l)
},
})
tests = append(tests, testCase{
description: "List() with partitioner All() error returned should returned an error.",
test: func(t *testing.T) {
p := NewMockPartitioner(gomock.NewController(t))
s := Store{
Partitioner: p,
}
req := &types.APIRequest{}
schema := &types.APISchema{
Schema: &schemas.Schema{},
}
p.EXPECT().All(req, schema, "list", "").Return(nil, fmt.Errorf("error"))
_, err := s.List(req, schema)
assert.NotNil(t, err)
},
})
tests = append(tests, testCase{
description: "List() with unstructured store ListByPartitions() error returned should returned an error.",
test: func(t *testing.T) {
p := NewMockPartitioner(gomock.NewController(t))
us := NewMockUnstructuredStore(gomock.NewController(t))
s := Store{
Partitioner: p,
}
req := &types.APIRequest{}
schema := &types.APISchema{
Schema: &schemas.Schema{},
}
partitions := make([]partition.Partition, 0)
p.EXPECT().All(req, schema, "list", "").Return(partitions, nil)
p.EXPECT().Store().Return(us)
us.EXPECT().ListByPartitions(req, schema, partitions).Return(nil, "", fmt.Errorf("error"))
_, err := s.List(req, schema)
assert.NotNil(t, err)
},
})
t.Parallel()
for _, test := range tests {
t.Run(test.description, func(t *testing.T) { test.test(t) })
}
}
type mockPartitioner struct {
store sqlproxy.Store
partitions map[string][]partition.Partition
}
func (m mockPartitioner) Lookup(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) (partition.Partition, error) {
panic("not implemented")
}
func (m mockPartitioner) All(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) ([]partition.Partition, error) {
user, _ := request.UserFrom(apiOp.Request.Context())
return m.partitions[user.GetName()], nil
}
func (m mockPartitioner) Store() sqlproxy.Store {
return m.store
}
type mockStore struct {
contents map[string]*unstructured.UnstructuredList
partition partition.Partition
called map[string]int
}
func (m *mockStore) WatchByPartitions(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest, partitions []partition.Partition) (chan watch.Event, error) {
//TODO implement me
panic("implement me")
}
func (m *mockStore) ListByPartitions(apiOp *types.APIRequest, schema *types.APISchema, partitions []partition.Partition) ([]unstructured.Unstructured, string, string, error) {
list := []unstructured.Unstructured{}
revision := ""
for _, partition := range partitions {
apiOp = apiOp.Clone()
apiOp.Namespace = partition.Namespace
partial, _, err := m.List(apiOp, schema)
if err != nil {
return nil, "", "", err
}
list = append(list, partial.Items...)
revision = partial.GetResourceVersion()
}
return list, revision, "", nil
}
func (m *mockStore) List(apiOp *types.APIRequest, schema *types.APISchema) (*unstructured.UnstructuredList, []types.Warning, error) {
n := apiOp.Namespace
previous, ok := m.called[n]
if !ok {
m.called[n] = 1
} else {
m.called[n] = previous + 1
}
query, _ := url.ParseQuery(apiOp.Request.URL.RawQuery)
l := query.Get("limit")
if l == "" {
return m.contents[n], nil, nil
}
i := 0
if c := query.Get("continue"); c != "" {
start, _ := base64.StdEncoding.DecodeString(c)
for j, obj := range m.contents[n].Items {
if string(start) == obj.GetName() {
i = j
break
}
}
}
lInt, _ := strconv.Atoi(l)
contents := m.contents[n].DeepCopy()
if len(contents.Items) > i+lInt {
contents.SetContinue(base64.StdEncoding.EncodeToString([]byte(contents.Items[i+lInt].GetName())))
}
if i > len(contents.Items) {
return contents, nil, nil
}
if i+lInt > len(contents.Items) {
contents.Items = contents.Items[i:]
return contents, nil, nil
}
contents.Items = contents.Items[i : i+lInt]
return contents, nil, nil
}
func (m *mockStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, []types.Warning, error) {
panic("not implemented")
}
func (m *mockStore) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (*unstructured.Unstructured, []types.Warning, error) {
panic("not implemented")
}
func (m *mockStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (*unstructured.Unstructured, []types.Warning, error) {
panic("not implemented")
}
func (m *mockStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, []types.Warning, error) {
panic("not implemented")
}
func (m *mockStore) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan watch.Event, error) {
panic("not implemented")
}
var colorMap = map[string]string{
"fuji": "pink",
"honeycrisp": "pink",
"granny-smith": "green",
"bramley": "green",
"crispin": "yellow",
"golden-delicious": "yellow",
"red-delicious": "red",
}
func newRequest(query, username string) *types.APIRequest {
return &types.APIRequest{
Request: (&http.Request{
URL: &url.URL{
Scheme: "https",
Host: "rancher",
Path: "/apples",
RawQuery: query,
},
}).WithContext(request.WithUser(context.Background(), &user.DefaultInfo{
Name: username,
Groups: []string{"system:authenticated"},
})),
}
}
type apple struct {
unstructured.Unstructured
}
func newApple(name string) apple {
return apple{unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": name,
},
"data": map[string]interface{}{
"color": colorMap[name],
},
},
}}
}
func (a apple) toObj() types.APIObject {
meta := a.Object["metadata"].(map[string]interface{})
id := meta["name"].(string)
ns, ok := meta["namespace"]
if ok {
id = ns.(string) + "/" + id
}
return types.APIObject{
Type: "apple",
ID: id,
Object: &a.Unstructured,
}
}
func (a apple) with(data map[string]string) apple {
for k, v := range data {
a.Object["data"].(map[string]interface{})[k] = v
}
return a
}
func (a apple) withNamespace(namespace string) apple {
a.Object["metadata"].(map[string]interface{})["namespace"] = namespace
return a
}
type mockAccessSetLookup struct {
accessID string
userRoles []map[string]string
}
func (m *mockAccessSetLookup) AccessFor(user user.Info) *accesscontrol.AccessSet {
userName := user.GetName()
access := getAccessID(userName, m.userRoles[0][userName])
m.userRoles = m.userRoles[1:]
return &accesscontrol.AccessSet{
ID: access,
}
}
func (m *mockAccessSetLookup) PurgeUserData(_ string) {
panic("not implemented")
}
func getAccessID(user, role string) string {
h := sha256.Sum256([]byte(user + role))
return string(h[:])
}
var namespaces = map[string]*corev1.Namespace{
"n1": &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "n1",
Labels: map[string]string{
"field.cattle.io/projectId": "p-abcde",
},
},
},
"n2": &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "n2",
Labels: map[string]string{
"field.cattle.io/projectId": "p-fghij",
},
},
},
"n3": &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "n3",
Labels: map[string]string{
"field.cattle.io/projectId": "p-klmno",
},
},
},
"n4": &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "n4",
},
},
}
type mockNamespaceCache struct{}
func (m mockNamespaceCache) Get(name string) (*corev1.Namespace, error) {
return namespaces[name], nil
}
func (m mockNamespaceCache) List(selector labels.Selector) ([]*corev1.Namespace, error) {
panic("not implemented")
}
func (m mockNamespaceCache) AddIndexer(indexName string, indexer generic.Indexer[*corev1.Namespace]) {
panic("not implemented")
}
func (m mockNamespaceCache) GetByIndex(indexName, key string) ([]*corev1.Namespace, error) {
panic("not implemented")
}