mirror of
https://github.com/rancher/steve.git
synced 2025-04-27 19:05:09 +00:00
* Move types related to list options and sql queries into their own package. The problem having these in the informer package is that eventually code in other packages will need to import `informer` only for constants or types, but some members of the informer package may already depend on those. Best to move type definitions into their own simpler package. * Fix the ListOptions sort field. Instead of making it a single array-ish field, convert it into a true array of Sort Directives. Easier to read, less bending backwards. * Pass the listOptions struct by reference to avoid copying. We never update the ListOptions struct once it's created so there's no need to pass it by value. This might be a near-useless optimization...
413 lines
17 KiB
Go
413 lines
17 KiB
Go
package informer
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rancher/steve/pkg/sqlcache/db"
|
|
"github.com/rancher/steve/pkg/sqlcache/partition"
|
|
"github.com/rancher/steve/pkg/sqlcache/sqltypes"
|
|
"github.com/stretchr/testify/assert"
|
|
"go.uber.org/mock/gomock"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/watch"
|
|
"k8s.io/client-go/tools/cache"
|
|
)
|
|
|
|
//go:generate mockgen --build_flags=--mod=mod -package informer -destination ./informer_mocks_test.go github.com/rancher/steve/pkg/sqlcache/informer ByOptionsLister
|
|
//go:generate mockgen --build_flags=--mod=mod -package informer -destination ./dynamic_mocks_test.go k8s.io/client-go/dynamic ResourceInterface
|
|
|
|
func TestNewInformer(t *testing.T) {
|
|
type testCase struct {
|
|
description string
|
|
test func(t *testing.T)
|
|
}
|
|
|
|
var tests []testCase
|
|
|
|
tests = append(tests, testCase{description: "NewInformer() with no errors returned, should return no error", test: func(t *testing.T) {
|
|
dbClient := NewMockClient(gomock.NewController(t))
|
|
txClient := NewMockTXClient(gomock.NewController(t))
|
|
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
|
|
|
fields := [][]string{{"something"}}
|
|
gvk := schema.GroupVersionKind{}
|
|
|
|
// NewStore() from store package logic. This package is only concerned with whether it returns err or not as NewStore
|
|
// is tested in depth in its own package.
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
dbClient.EXPECT().Prepare(gomock.Any()).Return(&sql.Stmt{}).AnyTimes()
|
|
|
|
// NewIndexer() logic (within NewListOptionIndexer(). This test is only concerned with whether it returns err or not as NewIndexer
|
|
// is tested in depth in its own indexer_test.go
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
// NewListOptionIndexer() logic. This test is only concerned with whether it returns err or not as NewIndexer
|
|
// is tested in depth in its own indexer_test.go
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
informer, err := NewInformer(context.Background(), dynamicClient, fields, nil, gvk, dbClient, false, true, true)
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, informer.ByOptionsLister)
|
|
assert.NotNil(t, informer.SharedIndexInformer)
|
|
}})
|
|
tests = append(tests, testCase{description: "NewInformer() with errors returned from NewStore(), should return an error", test: func(t *testing.T) {
|
|
dbClient := NewMockClient(gomock.NewController(t))
|
|
txClient := NewMockTXClient(gomock.NewController(t))
|
|
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
|
|
|
fields := [][]string{{"something"}}
|
|
gvk := schema.GroupVersionKind{}
|
|
|
|
// NewStore() from store package logic. This package is only concerned with whether it returns err or not as NewStore
|
|
// is tested in depth in its own package.
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error")).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
_, err := NewInformer(context.Background(), dynamicClient, fields, nil, gvk, dbClient, false, true, true)
|
|
assert.NotNil(t, err)
|
|
}})
|
|
tests = append(tests, testCase{description: "NewInformer() with errors returned from NewIndexer(), should return an error", test: func(t *testing.T) {
|
|
dbClient := NewMockClient(gomock.NewController(t))
|
|
txClient := NewMockTXClient(gomock.NewController(t))
|
|
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
|
|
|
fields := [][]string{{"something"}}
|
|
gvk := schema.GroupVersionKind{}
|
|
|
|
// NewStore() from store package logic. This package is only concerned with whether it returns err or not as NewStore
|
|
// is tested in depth in its own package.
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
dbClient.EXPECT().Prepare(gomock.Any()).Return(&sql.Stmt{}).AnyTimes()
|
|
|
|
// NewIndexer() logic (within NewListOptionIndexer(). This test is only concerned with whether it returns err or not as NewIndexer
|
|
// is tested in depth in its own indexer_test.go
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error")).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
_, err := NewInformer(context.Background(), dynamicClient, fields, nil, gvk, dbClient, false, true, true)
|
|
assert.NotNil(t, err)
|
|
}})
|
|
tests = append(tests, testCase{description: "NewInformer() with errors returned from NewListOptionIndexer(), should return an error", test: func(t *testing.T) {
|
|
dbClient := NewMockClient(gomock.NewController(t))
|
|
txClient := NewMockTXClient(gomock.NewController(t))
|
|
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
|
|
|
fields := [][]string{{"something"}}
|
|
gvk := schema.GroupVersionKind{}
|
|
|
|
// NewStore() from store package logic. This package is only concerned with whether it returns err or not as NewStore
|
|
// is tested in depth in its own package.
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
dbClient.EXPECT().Prepare(gomock.Any()).Return(&sql.Stmt{}).AnyTimes()
|
|
|
|
// NewIndexer() logic (within NewListOptionIndexer(). This test is only concerned with whether it returns err or not as NewIndexer
|
|
// is tested in depth in its own indexer_test.go
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
// NewListOptionIndexer() logic. This test is only concerned with whether it returns err or not as NewIndexer
|
|
// is tested in depth in its own indexer_test.go
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error")).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
_, err := NewInformer(context.Background(), dynamicClient, fields, nil, gvk, dbClient, false, true, true)
|
|
assert.NotNil(t, err)
|
|
}})
|
|
tests = append(tests, testCase{description: "NewInformer() with transform func", test: func(t *testing.T) {
|
|
dbClient := NewMockClient(gomock.NewController(t))
|
|
txClient := NewMockTXClient(gomock.NewController(t))
|
|
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
|
mockInformer := mockInformer{}
|
|
testNewInformer := func(lw cache.ListerWatcher,
|
|
exampleObject runtime.Object,
|
|
defaultEventHandlerResyncPeriod time.Duration,
|
|
indexers cache.Indexers) cache.SharedIndexInformer {
|
|
return &mockInformer
|
|
}
|
|
newInformer = testNewInformer
|
|
|
|
fields := [][]string{{"something"}}
|
|
gvk := schema.GroupVersionKind{}
|
|
|
|
// NewStore() from store package logic. This package is only concerned with whether it returns err or not as NewStore
|
|
// is tested in depth in its own package.
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
dbClient.EXPECT().Prepare(gomock.Any()).Return(&sql.Stmt{}).AnyTimes()
|
|
|
|
// NewIndexer() logic (within NewListOptionIndexer(). This test is only concerned with whether it returns err or not as NewIndexer
|
|
// is tested in depth in its own indexer_test.go
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
// NewListOptionIndexer() logic. This test is only concerned with whether it returns err or not as NewIndexer
|
|
// is tested in depth in its own indexer_test.go
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
transformFunc := func(input interface{}) (interface{}, error) {
|
|
return "someoutput", nil
|
|
}
|
|
informer, err := NewInformer(context.Background(), dynamicClient, fields, transformFunc, gvk, dbClient, false, true, true)
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, informer.ByOptionsLister)
|
|
assert.NotNil(t, informer.SharedIndexInformer)
|
|
assert.NotNil(t, mockInformer.transformFunc)
|
|
|
|
// we can't test func == func, so instead we check if the output was as expected
|
|
input := "someinput"
|
|
ouput, err := mockInformer.transformFunc(input)
|
|
assert.Nil(t, err)
|
|
outputStr, ok := ouput.(string)
|
|
assert.True(t, ok, "ouput from transform was expected to be a string")
|
|
assert.Equal(t, "someoutput", outputStr)
|
|
|
|
newInformer = cache.NewSharedIndexInformer
|
|
}})
|
|
tests = append(tests, testCase{description: "NewInformer() unable to set transform func", test: func(t *testing.T) {
|
|
dbClient := NewMockClient(gomock.NewController(t))
|
|
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
|
mockInformer := mockInformer{
|
|
setTranformErr: fmt.Errorf("some error"),
|
|
}
|
|
testNewInformer := func(lw cache.ListerWatcher,
|
|
exampleObject runtime.Object,
|
|
defaultEventHandlerResyncPeriod time.Duration,
|
|
indexers cache.Indexers) cache.SharedIndexInformer {
|
|
return &mockInformer
|
|
}
|
|
newInformer = testNewInformer
|
|
|
|
fields := [][]string{{"something"}}
|
|
gvk := schema.GroupVersionKind{}
|
|
|
|
transformFunc := func(input interface{}) (interface{}, error) {
|
|
return "someoutput", nil
|
|
}
|
|
_, err := NewInformer(context.Background(), dynamicClient, fields, transformFunc, gvk, dbClient, false, true, true)
|
|
assert.Error(t, err)
|
|
newInformer = cache.NewSharedIndexInformer
|
|
}})
|
|
|
|
t.Parallel()
|
|
for _, test := range tests {
|
|
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
|
}
|
|
}
|
|
|
|
func TestInformerListByOptions(t *testing.T) {
|
|
type testCase struct {
|
|
description string
|
|
test func(t *testing.T)
|
|
}
|
|
|
|
var tests []testCase
|
|
|
|
tests = append(tests, testCase{description: "ListByOptions() with no errors returned, should return no error and return value from indexer's ListByOptions()", test: func(t *testing.T) {
|
|
indexer := NewMockByOptionsLister(gomock.NewController(t))
|
|
informer := &Informer{
|
|
ByOptionsLister: indexer,
|
|
}
|
|
lo := sqltypes.ListOptions{}
|
|
var partitions []partition.Partition
|
|
ns := "somens"
|
|
expectedList := &unstructured.UnstructuredList{
|
|
Object: map[string]interface{}{"s": 2},
|
|
Items: []unstructured.Unstructured{{
|
|
Object: map[string]interface{}{"s": 2},
|
|
}},
|
|
}
|
|
expectedTotal := len(expectedList.Items)
|
|
expectedContinueToken := "123"
|
|
indexer.EXPECT().ListByOptions(context.Background(), &lo, partitions, ns).Return(expectedList, expectedTotal, expectedContinueToken, nil)
|
|
list, total, continueToken, err := informer.ListByOptions(context.Background(), &lo, partitions, ns)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, expectedList, list)
|
|
assert.Equal(t, len(expectedList.Items), total)
|
|
assert.Equal(t, expectedContinueToken, continueToken)
|
|
}})
|
|
tests = append(tests, testCase{description: "ListByOptions() with indexer ListByOptions error, should return error", test: func(t *testing.T) {
|
|
indexer := NewMockByOptionsLister(gomock.NewController(t))
|
|
informer := &Informer{
|
|
ByOptionsLister: indexer,
|
|
}
|
|
lo := sqltypes.ListOptions{}
|
|
var partitions []partition.Partition
|
|
ns := "somens"
|
|
indexer.EXPECT().ListByOptions(context.Background(), &lo, partitions, ns).Return(nil, 0, "", fmt.Errorf("error"))
|
|
_, _, _, err := informer.ListByOptions(context.Background(), &lo, partitions, ns)
|
|
assert.NotNil(t, err)
|
|
}})
|
|
t.Parallel()
|
|
for _, test := range tests {
|
|
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
|
}
|
|
}
|
|
|
|
// Note: SQLite based caching uses an Informer that unsafely sets the Indexer as the ability to set it is not present
|
|
// in client-go at the moment. Long term, we look forward contribute a patch to client-go to make that configurable.
|
|
// Until then, we are adding this canary test that will panic in case the indexer cannot be set.
|
|
func TestUnsafeSet(t *testing.T) {
|
|
listWatcher := &cache.ListWatch{
|
|
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
|
|
return &unstructured.UnstructuredList{}, nil
|
|
},
|
|
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
|
|
return dummyWatch{}, nil
|
|
},
|
|
}
|
|
|
|
sii := cache.NewSharedIndexInformer(listWatcher, &unstructured.Unstructured{}, 0, cache.Indexers{})
|
|
|
|
// will panic if SharedIndexInformer stops having a *Indexer field called "indexer"
|
|
UnsafeSet(sii, "indexer", &Indexer{})
|
|
}
|
|
|
|
type dummyWatch struct{}
|
|
|
|
func (dummyWatch) Stop() {
|
|
}
|
|
|
|
func (dummyWatch) ResultChan() <-chan watch.Event {
|
|
result := make(chan watch.Event)
|
|
defer close(result)
|
|
return result
|
|
}
|
|
|
|
// mockInformer is a mock of cache.SharedIndexInformer. Unlike other types, we can't generate this using mockgen because we use a unsafeSet to replace the
|
|
// indexer field, which is a struct field. This won't exist on the mock, producing an error. So we need to implement our own mock which actually has this field.
|
|
type mockInformer struct {
|
|
transformFunc cache.TransformFunc
|
|
setTranformErr error
|
|
indexer cache.Indexer
|
|
}
|
|
|
|
func (m *mockInformer) AddEventHandler(handler cache.ResourceEventHandler) (cache.ResourceEventHandlerRegistration, error) {
|
|
return nil, nil
|
|
}
|
|
func (m *mockInformer) AddEventHandlerWithResyncPeriod(handler cache.ResourceEventHandler, resyncPeriod time.Duration) (cache.ResourceEventHandlerRegistration, error) {
|
|
return nil, nil
|
|
}
|
|
func (m *mockInformer) RemoveEventHandler(handle cache.ResourceEventHandlerRegistration) error {
|
|
return nil
|
|
}
|
|
func (m *mockInformer) GetStore() cache.Store { return nil }
|
|
func (m *mockInformer) GetController() cache.Controller { return nil }
|
|
func (m *mockInformer) Run(stopCh <-chan struct{}) {}
|
|
func (m *mockInformer) HasSynced() bool { return false }
|
|
func (m *mockInformer) LastSyncResourceVersion() string { return "" }
|
|
func (m *mockInformer) SetWatchErrorHandler(handler cache.WatchErrorHandler) error { return nil }
|
|
func (m *mockInformer) IsStopped() bool { return false }
|
|
func (m *mockInformer) AddIndexers(indexers cache.Indexers) error { return nil }
|
|
func (m *mockInformer) GetIndexer() cache.Indexer { return nil }
|
|
func (m *mockInformer) SetTransform(handler cache.TransformFunc) error {
|
|
m.transformFunc = handler
|
|
return m.setTranformErr
|
|
}
|