1
0
mirror of https://github.com/rancher/steve.git synced 2025-07-15 23:46:34 +00:00
steve/pkg/sqlcache/informer/informer_test.go
Eric Promislow 3be82a28d1
Hard-wire external associations: 5/7: update A=>B links when instances of A change (#646)
* Continue rebasing.

* Wrote unit tests for external associations.

* Fix the generated SQL.

Some syntactic sugar (capitalizing the keywords), but use the 'ON' syntax on JOINs.

* We want "management.cattle.io.projects:spec.displayName" not "...spec.clusterName"

* Implement hard-wired external associations:

* The table is in sqlproxy.proxy_store
  - externalGVKDependencies - a map of GVKs to dependencies.
    When the key GVK is updated, it triggers the updates in the database for the dependent GVKs,
    replacing fields as specified in the table.

* This is done in an afterUpsert handler, but it's done after the transaction for the core
  GVK update is finished, because most likely the dependent GVK updates will depend on the
  final database values for the GVK being updated, and if we do it as part of the transaction
  the new values won't be committed to the database.

* When an object is modified/created, check for external deps that need updating.

* Stop emitting errors when joining tables if one of the tables doesn't exist.

* Update unit test syntax for SQL queries.

* And an override check

This ensures we don't overwrite good data when
pulling data from one table to another.

* Drop labels, and use mgmt.cattle.io/spec.displayName

There's no need to hardwire labels in proxy_store:typeSpecificIndexedFields
because all labels are indexed in the shadow labels table.

* Keep clusterName, add displayName for mgmt.cattle.io

* Fix rebase/merge breakage.

* Finish the merge: add the 'selfUpdateInfo' param where it didn't get inserted during merge.

* Patch up rebase failures.

* Now gomock generates named args. I give up.
2025-07-03 14:35:09 -07:00

429 lines
18 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)
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, nil, nil, gvk, dbClient, false, true, true, 0)
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, nil, nil, gvk, dbClient, false, true, true, 0)
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, nil, nil, gvk, dbClient, false, true, true, 0)
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)
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, nil, nil, gvk, dbClient, false, true, true, 0)
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)
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, nil, nil, transformFunc, gvk, dbClient, false, true, true, 0)
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"
output, err := mockInformer.transformFunc(input)
assert.Nil(t, err)
outputStr, ok := output.(string)
assert.True(t, ok, "output 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, nil, nil, transformFunc, gvk, dbClient, false, true, true, 0)
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
}
func (m *mockInformer) RunWithContext(ctx context.Context) {}
func (m *mockInformer) SetWatchErrorHandlerWithContext(handler cache.WatchErrorHandlerWithContext) error {
return nil
}
func (m *mockInformer) AddEventHandlerWithOptions(handler cache.ResourceEventHandler, options cache.HandlerOptions) (cache.ResourceEventHandlerRegistration, error) {
return nil, nil
}