1
0
mirror of https://github.com/rancher/steve.git synced 2025-04-28 03:10:32 +00:00
steve/pkg/stores/partition/store_test.go
Colleen Murphy b0eb50f38d Add caching to pagination
Cache filtered, sorted results for fast subsequent page retrieval.

Requests for cached queries need to include the list revision number
along with other queries. If no specific revision is requested, a new
fetch is done in order to get the latest revision. The revision is
included in the list response.

Example first request:

GET /v1/secrets?pagesize=10

Example subsequent page request:

GET /v1/secrets?pagesize=10&page=1&revision=107740
2022-12-20 15:01:19 -08:00

554 lines
14 KiB
Go

package partition
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strconv"
"testing"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/wrangler/pkg/schemas"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request"
)
func TestList(t *testing.T) {
tests := []struct {
name string
apiOps []*types.APIRequest
partitions []Partition
objects map[string]*unstructured.UnstructuredList
want []types.APIObjectList
}{
{
name: "basic",
apiOps: []*types.APIRequest{
newRequest("", "user1"),
},
partitions: []Partition{
mockPartition{
name: "all",
},
},
objects: map[string]*unstructured.UnstructuredList{
"all": {
Items: []unstructured.Unstructured{
newApple("fuji").Unstructured,
},
},
},
want: []types.APIObjectList{
{
Count: 1,
Objects: []types.APIObject{
newApple("fuji").toObj(),
},
},
},
},
{
name: "limit and continue",
apiOps: []*types.APIRequest{
newRequest("limit=1", "user1"),
newRequest(fmt.Sprintf("limit=1&continue=%s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"p":"all","c":"%s","l":1}`, base64.StdEncoding.EncodeToString([]byte("granny-smith")))))), "user1"),
newRequest(fmt.Sprintf("limit=1&continue=%s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"p":"all","c":"%s","l":1}`, base64.StdEncoding.EncodeToString([]byte("crispin")))))), "user1"),
},
partitions: []Partition{
mockPartition{
name: "all",
},
},
objects: map[string]*unstructured.UnstructuredList{
"all": {
Items: []unstructured.Unstructured{
newApple("fuji").Unstructured,
newApple("granny-smith").Unstructured,
newApple("crispin").Unstructured,
},
},
},
want: []types.APIObjectList{
{
Count: 1,
Continue: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"p":"all","c":"%s","l":1}`, base64.StdEncoding.EncodeToString([]byte("granny-smith"))))),
Objects: []types.APIObject{
newApple("fuji").toObj(),
},
},
{
Count: 1,
Continue: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"p":"all","c":"%s","l":1}`, base64.StdEncoding.EncodeToString([]byte("crispin"))))),
Objects: []types.APIObject{
newApple("granny-smith").toObj(),
},
},
{
Count: 1,
Objects: []types.APIObject{
newApple("crispin").toObj(),
},
},
},
},
{
name: "multi-partition",
apiOps: []*types.APIRequest{
newRequest("", "user1"),
},
partitions: []Partition{
mockPartition{
name: "green",
},
mockPartition{
name: "yellow",
},
},
objects: map[string]*unstructured.UnstructuredList{
"pink": {
Items: []unstructured.Unstructured{
newApple("fuji").Unstructured,
},
},
"green": {
Items: []unstructured.Unstructured{
newApple("granny-smith").Unstructured,
},
},
"yellow": {
Items: []unstructured.Unstructured{
newApple("crispin").Unstructured,
},
},
},
want: []types.APIObjectList{
{
Count: 2,
Objects: []types.APIObject{
newApple("granny-smith").toObj(),
newApple("crispin").toObj(),
},
},
},
},
{
name: "multi-partition with limit and continue",
apiOps: []*types.APIRequest{
newRequest("limit=3", "user1"),
newRequest(fmt.Sprintf("limit=3&continue=%s", base64.StdEncoding.EncodeToString([]byte(`{"p":"green","o":1,"l":3}`))), "user1"),
newRequest(fmt.Sprintf("limit=3&continue=%s", base64.StdEncoding.EncodeToString([]byte(`{"p":"red","l":3}`))), "user1"),
},
partitions: []Partition{
mockPartition{
name: "pink",
},
mockPartition{
name: "green",
},
mockPartition{
name: "yellow",
},
mockPartition{
name: "red",
},
},
objects: map[string]*unstructured.UnstructuredList{
"pink": {
Items: []unstructured.Unstructured{
newApple("fuji").Unstructured,
newApple("honeycrisp").Unstructured,
},
},
"green": {
Items: []unstructured.Unstructured{
newApple("granny-smith").Unstructured,
newApple("bramley").Unstructured,
},
},
"yellow": {
Items: []unstructured.Unstructured{
newApple("crispin").Unstructured,
newApple("golden-delicious").Unstructured,
},
},
"red": {
Items: []unstructured.Unstructured{
newApple("red-delicious").Unstructured,
},
},
},
want: []types.APIObjectList{
{
Count: 3,
Continue: base64.StdEncoding.EncodeToString([]byte(`{"p":"green","o":1,"l":3}`)),
Objects: []types.APIObject{
newApple("fuji").toObj(),
newApple("honeycrisp").toObj(),
newApple("granny-smith").toObj(),
},
},
{
Count: 3,
Continue: base64.StdEncoding.EncodeToString([]byte(`{"p":"red","l":3}`)),
Objects: []types.APIObject{
newApple("bramley").toObj(),
newApple("crispin").toObj(),
newApple("golden-delicious").toObj(),
},
},
{
Count: 1,
Objects: []types.APIObject{
newApple("red-delicious").toObj(),
},
},
},
},
{
name: "with filters",
apiOps: []*types.APIRequest{
newRequest("filter=data.color=green", "user1"),
newRequest("filter=data.color=green&filter=metadata.name=bramley", "user1"),
},
partitions: []Partition{
mockPartition{
name: "all",
},
},
objects: map[string]*unstructured.UnstructuredList{
"all": {
Items: []unstructured.Unstructured{
newApple("fuji").Unstructured,
newApple("granny-smith").Unstructured,
newApple("bramley").Unstructured,
newApple("crispin").Unstructured,
},
},
},
want: []types.APIObjectList{
{
Count: 2,
Objects: []types.APIObject{
newApple("granny-smith").toObj(),
newApple("bramley").toObj(),
},
},
{
Count: 1,
Objects: []types.APIObject{
newApple("bramley").toObj(),
},
},
},
},
{
name: "multi-partition with filters",
apiOps: []*types.APIRequest{
newRequest("filter=data.category=baking", "user1"),
},
partitions: []Partition{
mockPartition{
name: "pink",
},
mockPartition{
name: "green",
},
mockPartition{
name: "yellow",
},
},
objects: map[string]*unstructured.UnstructuredList{
"pink": {
Items: []unstructured.Unstructured{
newApple("fuji").with(map[string]string{"category": "eating"}).Unstructured,
newApple("honeycrisp").with(map[string]string{"category": "eating,baking"}).Unstructured,
},
},
"green": {
Items: []unstructured.Unstructured{
newApple("granny-smith").with(map[string]string{"category": "baking"}).Unstructured,
newApple("bramley").with(map[string]string{"category": "eating"}).Unstructured,
},
},
"yellow": {
Items: []unstructured.Unstructured{
newApple("crispin").with(map[string]string{"category": "baking"}).Unstructured,
},
},
},
want: []types.APIObjectList{
{
Count: 3,
Objects: []types.APIObject{
newApple("honeycrisp").with(map[string]string{"category": "eating,baking"}).toObj(),
newApple("granny-smith").with(map[string]string{"category": "baking"}).toObj(),
newApple("crispin").with(map[string]string{"category": "baking"}).toObj(),
},
},
},
},
{
name: "with sorting",
apiOps: []*types.APIRequest{
newRequest("sort=metadata.name", "user1"),
newRequest("sort=-metadata.name", "user1"),
},
partitions: []Partition{
mockPartition{
name: "all",
},
},
objects: map[string]*unstructured.UnstructuredList{
"all": {
Items: []unstructured.Unstructured{
newApple("fuji").Unstructured,
newApple("granny-smith").Unstructured,
newApple("bramley").Unstructured,
newApple("crispin").Unstructured,
},
},
},
want: []types.APIObjectList{
{
Count: 4,
Objects: []types.APIObject{
newApple("bramley").toObj(),
newApple("crispin").toObj(),
newApple("fuji").toObj(),
newApple("granny-smith").toObj(),
},
},
{
Count: 4,
Objects: []types.APIObject{
newApple("granny-smith").toObj(),
newApple("fuji").toObj(),
newApple("crispin").toObj(),
newApple("bramley").toObj(),
},
},
},
},
{
name: "multi-partition sort=metadata.name",
apiOps: []*types.APIRequest{
newRequest("sort=metadata.name", "user1"),
},
partitions: []Partition{
mockPartition{
name: "green",
},
mockPartition{
name: "yellow",
},
},
objects: map[string]*unstructured.UnstructuredList{
"pink": {
Items: []unstructured.Unstructured{
newApple("fuji").Unstructured,
},
},
"green": {
Items: []unstructured.Unstructured{
newApple("granny-smith").Unstructured,
},
},
"yellow": {
Items: []unstructured.Unstructured{
newApple("crispin").Unstructured,
},
},
},
want: []types.APIObjectList{
{
Count: 2,
Objects: []types.APIObject{
newApple("crispin").toObj(),
newApple("granny-smith").toObj(),
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
schema := &types.APISchema{Schema: &schemas.Schema{ID: "apple"}}
stores := map[string]*mockStore{}
for _, p := range test.partitions {
stores[p.Name()] = &mockStore{
contents: test.objects[p.Name()],
}
}
asl := &mockAccessSetLookup{}
store := NewStore(mockPartitioner{
stores: stores,
partitions: test.partitions,
}, asl)
for i, req := range test.apiOps {
got, gotErr := store.List(req, schema)
assert.Nil(t, gotErr)
assert.Equal(t, test.want[i], got)
}
})
}
}
type mockPartitioner struct {
stores map[string]*mockStore
partitions []Partition
}
func (m mockPartitioner) Lookup(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) (Partition, error) {
panic("not implemented")
}
func (m mockPartitioner) All(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) ([]Partition, error) {
return m.partitions, nil
}
func (m mockPartitioner) Store(apiOp *types.APIRequest, partition Partition) (UnstructuredStore, error) {
return m.stores[partition.Name()], nil
}
type mockPartition struct {
name string
}
func (m mockPartition) Name() string {
return m.name
}
type mockStore struct {
contents *unstructured.UnstructuredList
partition mockPartition
}
func (m *mockStore) List(apiOp *types.APIRequest, schema *types.APISchema) (*unstructured.UnstructuredList, error) {
query, _ := url.ParseQuery(apiOp.Request.URL.RawQuery)
l := query.Get("limit")
if l == "" {
return m.contents, nil
}
i := 0
if c := query.Get("continue"); c != "" {
start, _ := base64.StdEncoding.DecodeString(c)
for j, obj := range m.contents.Items {
if string(start) == obj.GetName() {
i = j
break
}
}
}
lInt, _ := strconv.Atoi(l)
contents := m.contents.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
}
if i+lInt > len(contents.Items) {
contents.Items = contents.Items[i:]
return contents, nil
}
contents.Items = contents.Items[i : i+lInt]
return contents, nil
}
func (m *mockStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, error) {
panic("not implemented")
}
func (m *mockStore) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (*unstructured.Unstructured, error) {
panic("not implemented")
}
func (m *mockStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (*unstructured.Unstructured, error) {
panic("not implemented")
}
func (m *mockStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, 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 {
return types.APIObject{
Type: "apple",
ID: a.Object["metadata"].(map[string]interface{})["name"].(string),
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
}
type mockAccessSetLookup struct{}
func (m *mockAccessSetLookup) AccessFor(_ user.Info) *accesscontrol.AccessSet {
return &accesscontrol.AccessSet{
ID: "aabbccdd",
}
}
func (m *mockAccessSetLookup) PurgeUserData(_ string) {
panic("not implemented")
}