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:
196
pkg/stores/sqlpartition/listprocessor/processor.go
Normal file
196
pkg/stores/sqlpartition/listprocessor/processor.go
Normal 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
|
||||
}
|
524
pkg/stores/sqlpartition/listprocessor/processor_test.go
Normal file
524
pkg/stores/sqlpartition/listprocessor/processor_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
54
pkg/stores/sqlpartition/listprocessor/proxy_mocks_test.go
Normal file
54
pkg/stores/sqlpartition/listprocessor/proxy_mocks_test.go
Normal 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)
|
||||
}
|
185
pkg/stores/sqlpartition/partition_mocks_test.go
Normal file
185
pkg/stores/sqlpartition/partition_mocks_test.go
Normal 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)
|
||||
}
|
118
pkg/stores/sqlpartition/partitioner.go
Normal file
118
pkg/stores/sqlpartition/partitioner.go
Normal 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
|
||||
}
|
263
pkg/stores/sqlpartition/partitioner_test.go
Normal file
263
pkg/stores/sqlpartition/partitioner_test.go
Normal 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)
|
||||
}
|
142
pkg/stores/sqlpartition/store.go
Normal file
142
pkg/stores/sqlpartition/store.go
Normal 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
|
||||
}
|
383
pkg/stores/sqlpartition/store_test.go
Normal file
383
pkg/stores/sqlpartition/store_test.go
Normal 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")
|
||||
}
|
Reference in New Issue
Block a user