Merge pull request #105 from cmurphy/projects-filtering

Add projectsornamespaces query parameter
This commit is contained in:
Colleen Murphy 2023-05-12 10:58:54 -07:00 committed by GitHub
commit 1dfd3c711f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 985 additions and 38 deletions

View File

@ -117,6 +117,36 @@ item is included in the list.
/v1/{type}?filter=spec.containers.image=alpine /v1/{type}?filter=spec.containers.image=alpine
``` ```
#### `projectsornamespaces`
Resources can also be filtered by the Rancher projects their namespaces belong
to. Since a project isn't an intrinsic part of the resource itself, the filter
parameter for filtering by projects is separate from the main `filter`
parameter. This query parameter is only applicable when steve is runnning in
concert with Rancher.
The list can be filtered by either projects or namespaces or both.
Filtering by a single project or a single namespace:
```
/v1/{type}?projectsornamespaces=p1
```
Filtering by multiple projects or namespaces is done with a comma separated
list. A resource matching any project or namespace in the list is included in
the result:
```
/v1/{type}?projectsornamespaces=p1,n1,n2
```
The list can be negated to exclude results:
```
/v1/{type}?projectsornamespaces!=p1,n1,n2
```
#### `sort` #### `sort`
Only applicable to list requests (`/v1/{type}` and `/v1/{type}/{namespace}`). Only applicable to list requests (`/v1/{type}` and `/v1/{type}/{namespace}`).

View File

@ -11,6 +11,7 @@ import (
"github.com/rancher/steve/pkg/stores/proxy" "github.com/rancher/steve/pkg/stores/proxy"
"github.com/rancher/steve/pkg/summarycache" "github.com/rancher/steve/pkg/summarycache"
"github.com/rancher/wrangler/pkg/data" "github.com/rancher/wrangler/pkg/data"
corecontrollers "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
"github.com/rancher/wrangler/pkg/slice" "github.com/rancher/wrangler/pkg/slice"
"github.com/rancher/wrangler/pkg/summary" "github.com/rancher/wrangler/pkg/summary"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
@ -21,9 +22,10 @@ import (
func DefaultTemplate(clientGetter proxy.ClientGetter, func DefaultTemplate(clientGetter proxy.ClientGetter,
summaryCache *summarycache.SummaryCache, summaryCache *summarycache.SummaryCache,
asl accesscontrol.AccessSetLookup) schema.Template { asl accesscontrol.AccessSetLookup,
namespaceCache corecontrollers.NamespaceCache) schema.Template {
return schema.Template{ return schema.Template{
Store: metricsStore.NewMetricsStore(proxy.NewProxyStore(clientGetter, summaryCache, asl)), Store: metricsStore.NewMetricsStore(proxy.NewProxyStore(clientGetter, summaryCache, asl, namespaceCache)),
Formatter: formatter(summaryCache), Formatter: formatter(summaryCache),
} }
} }

View File

@ -19,6 +19,7 @@ import (
steveschema "github.com/rancher/steve/pkg/schema" steveschema "github.com/rancher/steve/pkg/schema"
"github.com/rancher/steve/pkg/stores/proxy" "github.com/rancher/steve/pkg/stores/proxy"
"github.com/rancher/steve/pkg/summarycache" "github.com/rancher/steve/pkg/summarycache"
corecontrollers "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/client-go/discovery" "k8s.io/client-go/discovery"
) )
@ -46,9 +47,10 @@ func DefaultSchemaTemplates(cf *client.Factory,
baseSchemas *types.APISchemas, baseSchemas *types.APISchemas,
summaryCache *summarycache.SummaryCache, summaryCache *summarycache.SummaryCache,
lookup accesscontrol.AccessSetLookup, lookup accesscontrol.AccessSetLookup,
discovery discovery.DiscoveryInterface) []schema.Template { discovery discovery.DiscoveryInterface,
namespaceCache corecontrollers.NamespaceCache) []schema.Template {
return []schema.Template{ return []schema.Template{
common.DefaultTemplate(cf, summaryCache, lookup), common.DefaultTemplate(cf, summaryCache, lookup, namespaceCache),
apigroups.Template(discovery), apigroups.Template(discovery),
{ {
ID: "configmap", ID: "configmap",

View File

@ -145,7 +145,7 @@ func setup(ctx context.Context, server *Server) error {
summaryCache := summarycache.New(sf, ccache) summaryCache := summarycache.New(sf, ccache)
summaryCache.Start(ctx) summaryCache.Start(ctx)
for _, template := range resources.DefaultSchemaTemplates(cf, server.BaseSchemas, summaryCache, asl, server.controllers.K8s.Discovery()) { for _, template := range resources.DefaultSchemaTemplates(cf, server.BaseSchemas, summaryCache, asl, server.controllers.K8s.Discovery(), server.controllers.Core.Namespace().Cache()) {
sf.AddTemplate(template) sf.AddTemplate(template)
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/rancher/apiserver/pkg/types" "github.com/rancher/apiserver/pkg/types"
"github.com/rancher/wrangler/pkg/data" "github.com/rancher/wrangler/pkg/data"
"github.com/rancher/wrangler/pkg/data/convert" "github.com/rancher/wrangler/pkg/data/convert"
corecontrollers "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
) )
@ -22,7 +23,11 @@ const (
pageSizeParam = "pagesize" pageSizeParam = "pagesize"
pageParam = "page" pageParam = "page"
revisionParam = "revision" revisionParam = "revision"
projectsOrNamespacesVar = "projectsornamespaces"
projectIDFieldLabel = "field.cattle.io/projectId"
orOp = "," orOp = ","
notOp = "!"
) )
var opReg = regexp.MustCompile(`[!]?=`) var opReg = regexp.MustCompile(`[!]?=`)
@ -42,6 +47,7 @@ type ListOptions struct {
Sort Sort Sort Sort
Pagination Pagination Pagination Pagination
Revision string Revision string
ProjectsOrNamespaces ProjectsOrNamespacesFilter
} }
// Filter represents a field to filter by. // Filter represents a field to filter by.
@ -127,11 +133,21 @@ func (p Pagination) PageSize() int {
return p.pageSize return p.pageSize
} }
type ProjectsOrNamespacesFilter struct {
filter map[string]struct{}
op op
}
// ParseQuery parses the query params of a request and returns a ListOptions. // ParseQuery parses the query params of a request and returns a ListOptions.
func ParseQuery(apiOp *types.APIRequest) *ListOptions { func ParseQuery(apiOp *types.APIRequest) *ListOptions {
chunkSize := getLimit(apiOp) opts := ListOptions{}
opts.ChunkSize = getLimit(apiOp)
q := apiOp.Request.URL.Query() q := apiOp.Request.URL.Query()
cont := q.Get(continueParam) cont := q.Get(continueParam)
opts.Resume = cont
filterParams := q[filterParam] filterParams := q[filterParam]
filterOpts := []OrFilter{} filterOpts := []OrFilter{}
for _, filters := range filterParams { for _, filters := range filterParams {
@ -168,6 +184,8 @@ func ParseQuery(apiOp *types.APIRequest) *ListOptions {
} }
return fieldI.String() < fieldJ.String() return fieldI.String() < fieldJ.String()
}) })
opts.Filters = filterOpts
sortOpts := Sort{} sortOpts := Sort{}
sortKeys := q.Get(sortParam) sortKeys := q.Get(sortParam)
if sortKeys != "" { if sortKeys != "" {
@ -191,6 +209,8 @@ func ParseQuery(apiOp *types.APIRequest) *ListOptions {
} }
} }
} }
opts.Sort = sortOpts
var err error var err error
pagination := Pagination{} pagination := Pagination{}
pagination.pageSize, err = strconv.Atoi(q.Get(pageSizeParam)) pagination.pageSize, err = strconv.Atoi(q.Get(pageSizeParam))
@ -201,16 +221,30 @@ func ParseQuery(apiOp *types.APIRequest) *ListOptions {
if err != nil { if err != nil {
pagination.page = 1 pagination.page = 1
} }
opts.Pagination = pagination
revision := q.Get(revisionParam) revision := q.Get(revisionParam)
return &ListOptions{ opts.Revision = revision
ChunkSize: chunkSize,
Resume: cont, projectsOptions := ProjectsOrNamespacesFilter{}
Filters: filterOpts, var op op
Sort: sortOpts, projectsOrNamespaces := q.Get(projectsOrNamespacesVar)
Pagination: pagination, if projectsOrNamespaces == "" {
Revision: revision, projectsOrNamespaces = q.Get(projectsOrNamespacesVar + notOp)
if projectsOrNamespaces != "" {
op = notEq
} }
} }
if projectsOrNamespaces != "" {
projectsOptions.filter = make(map[string]struct{})
for _, pn := range strings.Split(projectsOrNamespaces, ",") {
projectsOptions.filter[pn] = struct{}{}
}
projectsOptions.op = op
opts.ProjectsOrNamespaces = projectsOptions
}
return &opts
}
// getLimit extracts the limit parameter from the request or sets a default of 100000. // 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. // The default limit can be explicitly disabled by setting it to zero or negative.
@ -360,3 +394,31 @@ func PaginateList(list []unstructured.Unstructured, p Pagination) ([]unstructure
} }
return list[offset : offset+p.pageSize], pages return list[offset : offset+p.pageSize], pages
} }
func FilterByProjectsAndNamespaces(list []unstructured.Unstructured, projectsOrNamespaces ProjectsOrNamespacesFilter, namespaceCache corecontrollers.NamespaceCache) []unstructured.Unstructured {
if len(projectsOrNamespaces.filter) == 0 {
return list
}
result := []unstructured.Unstructured{}
for _, obj := range list {
namespaceName := obj.GetNamespace()
if namespaceName == "" {
continue
}
namespace, err := namespaceCache.Get(namespaceName)
if namespace == nil || err != nil {
continue
}
projectLabel, _ := namespace.GetLabels()[projectIDFieldLabel]
_, matchesProject := projectsOrNamespaces.filter[projectLabel]
_, matchesNamespace := projectsOrNamespaces.filter[namespaceName]
matches := matchesProject || matchesNamespace
if projectsOrNamespaces.op == eq && matches {
result = append(result, obj)
}
if projectsOrNamespaces.op == notEq && !matches {
result = append(result, obj)
}
}
return result
}

View File

@ -3,8 +3,12 @@ package listprocessor
import ( import (
"testing" "testing"
corecontrollers "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
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/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
) )
func TestFilterList(t *testing.T) { func TestFilterList(t *testing.T) {
@ -2567,3 +2571,669 @@ func TestPaginateList(t *testing.T) {
}) })
} }
} }
func TestFilterByProjectsAndNamespaces(t *testing.T) {
tests := []struct {
name string
objects []unstructured.Unstructured
filter ProjectsOrNamespacesFilter
want []unstructured.Unstructured
}{
{
name: "filter by one project",
objects: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n2",
},
},
},
},
filter: ProjectsOrNamespacesFilter{
filter: map[string]struct{}{
"p-abcde": struct{}{},
},
op: eq,
},
want: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
},
},
{
name: "filter by multiple projects",
objects: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
filter: ProjectsOrNamespacesFilter{
filter: map[string]struct{}{
"p-abcde": struct{}{},
"p-fghij": struct{}{},
},
},
want: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
},
},
{
name: "filter by one namespace",
objects: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n2",
},
},
},
},
filter: ProjectsOrNamespacesFilter{
filter: map[string]struct{}{
"n1": struct{}{},
},
op: eq,
},
want: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
},
},
{
name: "filter by multiple namespaces",
objects: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
filter: ProjectsOrNamespacesFilter{
filter: map[string]struct{}{
"n1": struct{}{},
"n2": struct{}{},
},
},
want: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
},
},
{
name: "filter by namespaces and projects",
objects: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
filter: ProjectsOrNamespacesFilter{
filter: map[string]struct{}{
"n1": struct{}{},
"p-fghij": struct{}{},
},
},
want: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
},
},
{
name: "no matches",
objects: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
filter: ProjectsOrNamespacesFilter{
filter: map[string]struct{}{
"foobar": struct{}{},
},
},
want: []unstructured.Unstructured{},
},
{
name: "no filters",
objects: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
filter: ProjectsOrNamespacesFilter{},
want: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
},
{
name: "filter by one project negated",
objects: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
filter: ProjectsOrNamespacesFilter{
filter: map[string]struct{}{
"p-abcde": struct{}{},
},
op: notEq,
},
want: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
},
{
name: "filter by multiple projects negated",
objects: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
filter: ProjectsOrNamespacesFilter{
filter: map[string]struct{}{
"p-abcde": struct{}{},
"p-fghij": struct{}{},
},
op: notEq,
},
want: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
},
{
name: "filter by one namespace negated",
objects: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n2",
},
},
},
},
filter: ProjectsOrNamespacesFilter{
filter: map[string]struct{}{
"n1": struct{}{},
},
op: notEq,
},
want: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n2",
},
},
},
},
},
{
name: "filter by multiple namespaces negated",
objects: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
filter: ProjectsOrNamespacesFilter{
filter: map[string]struct{}{
"n1": struct{}{},
"n2": struct{}{},
},
op: notEq,
},
want: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
},
{
name: "filter by namespaces and projects negated",
objects: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "n1",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "honeycrisp",
"namespace": "n2",
},
},
},
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
filter: ProjectsOrNamespacesFilter{
filter: map[string]struct{}{
"n1": struct{}{},
"p-fghij": struct{}{},
},
op: notEq,
},
want: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "granny-smith",
"namespace": "n3",
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := FilterByProjectsAndNamespaces(test.objects, test.filter, mockNamespaceCache{})
assert.Equal(t, test.want, got)
})
}
}
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 corecontrollers.NamespaceIndexer) {
panic("not implemented")
}
func (m mockNamespaceCache) GetByIndex(indexName, key string) ([]*corev1.Namespace, error) {
panic("not implemented")
}

View File

@ -13,6 +13,7 @@ import (
"github.com/rancher/apiserver/pkg/types" "github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/accesscontrol" "github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/stores/partition/listprocessor" "github.com/rancher/steve/pkg/stores/partition/listprocessor"
corecontrollers "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
@ -45,10 +46,11 @@ type Store struct {
Partitioner Partitioner Partitioner Partitioner
listCache *cache.LRUExpireCache listCache *cache.LRUExpireCache
asl accesscontrol.AccessSetLookup asl accesscontrol.AccessSetLookup
namespaceCache corecontrollers.NamespaceCache
} }
// NewStore creates a types.Store implementation with a partitioner and an LRU expiring cache for list responses. // NewStore creates a types.Store implementation with a partitioner and an LRU expiring cache for list responses.
func NewStore(partitioner Partitioner, asl accesscontrol.AccessSetLookup) *Store { func NewStore(partitioner Partitioner, asl accesscontrol.AccessSetLookup, namespaceCache corecontrollers.NamespaceCache) *Store {
cacheSize := defaultCacheSize cacheSize := defaultCacheSize
if v := os.Getenv(cacheSizeEnv); v != "" { if v := os.Getenv(cacheSizeEnv); v != "" {
sizeInt, err := strconv.Atoi(v) sizeInt, err := strconv.Atoi(v)
@ -59,6 +61,7 @@ func NewStore(partitioner Partitioner, asl accesscontrol.AccessSetLookup) *Store
s := &Store{ s := &Store{
Partitioner: partitioner, Partitioner: partitioner,
asl: asl, asl: asl,
namespaceCache: namespaceCache,
} }
if v := os.Getenv(cacheDisableEnv); v == "false" { if v := os.Getenv(cacheDisableEnv); v == "false" {
s.listCache = cache.NewLRUExpireCache(cacheSize) s.listCache = cache.NewLRUExpireCache(cacheSize)
@ -203,6 +206,7 @@ func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.AP
listToCache := &unstructured.UnstructuredList{ listToCache := &unstructured.UnstructuredList{
Items: list, Items: list,
} }
list = listprocessor.FilterByProjectsAndNamespaces(list, opts.ProjectsOrNamespaces, s.namespaceCache)
c := lister.Continue() c := lister.Continue()
if c != "" { if c != "" {
listToCache.SetContinue(c) listToCache.SetContinue(c)

View File

@ -12,9 +12,13 @@ import (
"github.com/rancher/apiserver/pkg/types" "github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/accesscontrol" "github.com/rancher/steve/pkg/accesscontrol"
corecontrollers "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
"github.com/rancher/wrangler/pkg/schemas" "github.com/rancher/wrangler/pkg/schemas"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
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/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
@ -1928,6 +1932,118 @@ func TestList(t *testing.T) {
{"all": 1}, {"all": 1},
}, },
}, },
{
name: "with project filters",
apiOps: []*types.APIRequest{
newRequest("projectsornamespaces=p-abcde", "user1"),
newRequest("projectsornamespaces=p-abcde,p-fghij", "user1"),
newRequest("projectsornamespaces=p-abcde,n2", "user1"),
newRequest("projectsornamespaces!=p-abcde", "user1"),
newRequest("projectsornamespaces!=p-abcde,p-fghij", "user1"),
newRequest("projectsornamespaces!=p-abcde,n2", "user1"),
newRequest("projectsornamespaces=foobar", "user1"),
newRequest("projectsornamespaces!=foobar", "user1"),
},
access: []map[string]string{
{
"user1": "roleA",
},
{
"user1": "roleA",
},
{
"user1": "roleA",
},
{
"user1": "roleA",
},
{
"user1": "roleA",
},
{
"user1": "roleA",
},
{
"user1": "roleA",
},
{
"user1": "roleA",
},
},
partitions: map[string][]Partition{
"user1": {
mockPartition{
name: "all",
},
},
},
objects: map[string]*unstructured.UnstructuredList{
"all": {
Items: []unstructured.Unstructured{
newApple("fuji").withNamespace("n1").Unstructured,
newApple("granny-smith").withNamespace("n1").Unstructured,
newApple("bramley").withNamespace("n2").Unstructured,
newApple("crispin").withNamespace("n3").Unstructured,
},
},
},
want: []types.APIObjectList{
{
Count: 2,
Objects: []types.APIObject{
newApple("fuji").withNamespace("n1").toObj(),
newApple("granny-smith").withNamespace("n1").toObj(),
},
},
{
Count: 3,
Objects: []types.APIObject{
newApple("fuji").withNamespace("n1").toObj(),
newApple("granny-smith").withNamespace("n1").toObj(),
newApple("bramley").withNamespace("n2").toObj(),
},
},
{
Count: 3,
Objects: []types.APIObject{
newApple("fuji").withNamespace("n1").toObj(),
newApple("granny-smith").withNamespace("n1").toObj(),
newApple("bramley").withNamespace("n2").toObj(),
},
},
{
Count: 2,
Objects: []types.APIObject{
newApple("bramley").withNamespace("n2").toObj(),
newApple("crispin").withNamespace("n3").toObj(),
},
},
{
Count: 1,
Objects: []types.APIObject{
newApple("crispin").withNamespace("n3").toObj(),
},
},
{
Count: 1,
Objects: []types.APIObject{
newApple("crispin").withNamespace("n3").toObj(),
},
},
{
Count: 0,
},
{
Count: 4,
Objects: []types.APIObject{
newApple("fuji").withNamespace("n1").toObj(),
newApple("granny-smith").withNamespace("n1").toObj(),
newApple("bramley").withNamespace("n2").toObj(),
newApple("crispin").withNamespace("n3").toObj(),
},
},
},
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
@ -1947,7 +2063,7 @@ func TestList(t *testing.T) {
store := NewStore(mockPartitioner{ store := NewStore(mockPartitioner{
stores: stores, stores: stores,
partitions: test.partitions, partitions: test.partitions,
}, asl) }, asl, mockNamespaceCache{})
for i, req := range test.apiOps { for i, req := range test.apiOps {
got, gotErr := store.List(req, schema) got, gotErr := store.List(req, schema)
assert.Nil(t, gotErr) assert.Nil(t, gotErr)
@ -2022,7 +2138,7 @@ func TestListByRevision(t *testing.T) {
}, },
}, },
}, },
}, asl) }, asl, mockNamespaceCache{})
req := newRequest("", "user1") req := newRequest("", "user1")
got, gotErr := store.List(req, schema) got, gotErr := store.List(req, schema)
@ -2214,9 +2330,15 @@ func newApple(name string) apple {
} }
func (a apple) toObj() types.APIObject { 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{ return types.APIObject{
Type: "apple", Type: "apple",
ID: a.Object["metadata"].(map[string]interface{})["name"].(string), ID: id,
Object: &a.Unstructured, Object: &a.Unstructured,
} }
} }
@ -2228,6 +2350,11 @@ func (a apple) with(data map[string]string) apple {
return a return a
} }
func (a apple) withNamespace(namespace string) apple {
a.Object["metadata"].(map[string]interface{})["namespace"] = namespace
return a
}
type mockAccessSetLookup struct { type mockAccessSetLookup struct {
accessID string accessID string
userRoles []map[string]string userRoles []map[string]string
@ -2250,3 +2377,51 @@ func getAccessID(user, role string) string {
h := sha256.Sum256([]byte(user + role)) h := sha256.Sum256([]byte(user + role))
return string(h[:]) 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 corecontrollers.NamespaceIndexer) {
panic("not implemented")
}
func (m mockNamespaceCache) GetByIndex(indexName, key string) ([]*corev1.Namespace, error) {
panic("not implemented")
}

View File

@ -19,6 +19,7 @@ import (
metricsStore "github.com/rancher/steve/pkg/stores/metrics" metricsStore "github.com/rancher/steve/pkg/stores/metrics"
"github.com/rancher/steve/pkg/stores/partition" "github.com/rancher/steve/pkg/stores/partition"
"github.com/rancher/wrangler/pkg/data" "github.com/rancher/wrangler/pkg/data"
corecontrollers "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
"github.com/rancher/wrangler/pkg/schemas/validation" "github.com/rancher/wrangler/pkg/schemas/validation"
"github.com/rancher/wrangler/pkg/summary" "github.com/rancher/wrangler/pkg/summary"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -85,7 +86,7 @@ type Store struct {
} }
// NewProxyStore returns a wrapped types.Store. // NewProxyStore returns a wrapped types.Store.
func NewProxyStore(clientGetter ClientGetter, notifier RelationshipNotifier, lookup accesscontrol.AccessSetLookup) types.Store { func NewProxyStore(clientGetter ClientGetter, notifier RelationshipNotifier, lookup accesscontrol.AccessSetLookup, namespaceCache corecontrollers.NamespaceCache) types.Store {
return &errorStore{ return &errorStore{
Store: &WatchRefresh{ Store: &WatchRefresh{
Store: partition.NewStore( Store: partition.NewStore(
@ -96,6 +97,7 @@ func NewProxyStore(clientGetter ClientGetter, notifier RelationshipNotifier, loo
}, },
}, },
lookup, lookup,
namespaceCache,
), ),
asl: lookup, asl: lookup,
}, },