mirror of
https://github.com/rancher/steve.git
synced 2025-09-15 23:08:26 +00:00
[v0.3] Migrate SQL cache to Steve (#474)
* Migrate SQLcache to Steve * Fix imports * go mod tidy * Fix lint errors * Remove lasso SQL cache mentions * Fix more CI lint errors * fix goimports Signed-off-by: Silvio Moioli <silvio@moioli.net> * Fix more linting errors * More lint fix * Add envtest support --------- Signed-off-by: Silvio Moioli <silvio@moioli.net> Co-authored-by: Silvio Moioli <silvio@moioli.net>
This commit is contained in:
204
pkg/sqlcache/informer/db_mocks_test.go
Normal file
204
pkg/sqlcache/informer/db_mocks_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/rancher/steve/pkg/sqlcache/db (interfaces: TXClient,Rows)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen --build_flags=--mod=mod -package informer -destination ./db_mocks_test.go github.com/rancher/steve/pkg/sqlcache/db TXClient,Rows
|
||||
//
|
||||
|
||||
// Package informer is a generated GoMock package.
|
||||
package informer
|
||||
|
||||
import (
|
||||
sql "database/sql"
|
||||
reflect "reflect"
|
||||
|
||||
transaction "github.com/rancher/steve/pkg/sqlcache/db/transaction"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockTXClient is a mock of TXClient interface.
|
||||
type MockTXClient struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockTXClientMockRecorder
|
||||
}
|
||||
|
||||
// MockTXClientMockRecorder is the mock recorder for MockTXClient.
|
||||
type MockTXClientMockRecorder struct {
|
||||
mock *MockTXClient
|
||||
}
|
||||
|
||||
// NewMockTXClient creates a new mock instance.
|
||||
func NewMockTXClient(ctrl *gomock.Controller) *MockTXClient {
|
||||
mock := &MockTXClient{ctrl: ctrl}
|
||||
mock.recorder = &MockTXClientMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockTXClient) EXPECT() *MockTXClientMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Cancel mocks base method.
|
||||
func (m *MockTXClient) Cancel() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Cancel")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Cancel indicates an expected call of Cancel.
|
||||
func (mr *MockTXClientMockRecorder) Cancel() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cancel", reflect.TypeOf((*MockTXClient)(nil).Cancel))
|
||||
}
|
||||
|
||||
// Commit mocks base method.
|
||||
func (m *MockTXClient) Commit() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Commit")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Commit indicates an expected call of Commit.
|
||||
func (mr *MockTXClientMockRecorder) Commit() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockTXClient)(nil).Commit))
|
||||
}
|
||||
|
||||
// Exec mocks base method.
|
||||
func (m *MockTXClient) Exec(arg0 string, arg1 ...any) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Exec", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Exec indicates an expected call of Exec.
|
||||
func (mr *MockTXClientMockRecorder) Exec(arg0 any, arg1 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockTXClient)(nil).Exec), varargs...)
|
||||
}
|
||||
|
||||
// Stmt mocks base method.
|
||||
func (m *MockTXClient) Stmt(arg0 *sql.Stmt) transaction.Stmt {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Stmt", arg0)
|
||||
ret0, _ := ret[0].(transaction.Stmt)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Stmt indicates an expected call of Stmt.
|
||||
func (mr *MockTXClientMockRecorder) Stmt(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stmt", reflect.TypeOf((*MockTXClient)(nil).Stmt), arg0)
|
||||
}
|
||||
|
||||
// StmtExec mocks base method.
|
||||
func (m *MockTXClient) StmtExec(arg0 transaction.Stmt, arg1 ...any) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "StmtExec", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// StmtExec indicates an expected call of StmtExec.
|
||||
func (mr *MockTXClientMockRecorder) StmtExec(arg0 any, arg1 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StmtExec", reflect.TypeOf((*MockTXClient)(nil).StmtExec), varargs...)
|
||||
}
|
||||
|
||||
// MockRows is a mock of Rows interface.
|
||||
type MockRows struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockRowsMockRecorder
|
||||
}
|
||||
|
||||
// MockRowsMockRecorder is the mock recorder for MockRows.
|
||||
type MockRowsMockRecorder struct {
|
||||
mock *MockRows
|
||||
}
|
||||
|
||||
// NewMockRows creates a new mock instance.
|
||||
func NewMockRows(ctrl *gomock.Controller) *MockRows {
|
||||
mock := &MockRows{ctrl: ctrl}
|
||||
mock.recorder = &MockRowsMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockRows) EXPECT() *MockRowsMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Close mocks base method.
|
||||
func (m *MockRows) Close() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Close")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Close indicates an expected call of Close.
|
||||
func (mr *MockRowsMockRecorder) Close() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRows)(nil).Close))
|
||||
}
|
||||
|
||||
// Err mocks base method.
|
||||
func (m *MockRows) Err() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Err")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Err indicates an expected call of Err.
|
||||
func (mr *MockRowsMockRecorder) Err() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Err", reflect.TypeOf((*MockRows)(nil).Err))
|
||||
}
|
||||
|
||||
// Next mocks base method.
|
||||
func (m *MockRows) Next() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Next")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Next indicates an expected call of Next.
|
||||
func (mr *MockRowsMockRecorder) Next() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockRows)(nil).Next))
|
||||
}
|
||||
|
||||
// Scan mocks base method.
|
||||
func (m *MockRows) Scan(arg0 ...any) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{}
|
||||
for _, a := range arg0 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Scan", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Scan indicates an expected call of Scan.
|
||||
func (mr *MockRowsMockRecorder) Scan(arg0 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scan", reflect.TypeOf((*MockRows)(nil).Scan), arg0...)
|
||||
}
|
237
pkg/sqlcache/informer/dynamic_mocks_test.go
Normal file
237
pkg/sqlcache/informer/dynamic_mocks_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: k8s.io/client-go/dynamic (interfaces: ResourceInterface)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen --build_flags=--mod=mod -package informer -destination ./dynamic_mocks_test.go k8s.io/client-go/dynamic ResourceInterface
|
||||
//
|
||||
|
||||
// Package informer is a generated GoMock package.
|
||||
package informer
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
types "k8s.io/apimachinery/pkg/types"
|
||||
watch "k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
// MockResourceInterface is a mock of ResourceInterface interface.
|
||||
type MockResourceInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockResourceInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockResourceInterfaceMockRecorder is the mock recorder for MockResourceInterface.
|
||||
type MockResourceInterfaceMockRecorder struct {
|
||||
mock *MockResourceInterface
|
||||
}
|
||||
|
||||
// NewMockResourceInterface creates a new mock instance.
|
||||
func NewMockResourceInterface(ctrl *gomock.Controller) *MockResourceInterface {
|
||||
mock := &MockResourceInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockResourceInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockResourceInterface) EXPECT() *MockResourceInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Apply mocks base method.
|
||||
func (m *MockResourceInterface) Apply(arg0 context.Context, arg1 string, arg2 *unstructured.Unstructured, arg3 v1.ApplyOptions, arg4 ...string) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2, arg3}
|
||||
for _, a := range arg4 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Apply", varargs...)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Apply indicates an expected call of Apply.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Apply(arg0, arg1, arg2, arg3 any, arg4 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1, arg2, arg3}, arg4...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockResourceInterface)(nil).Apply), varargs...)
|
||||
}
|
||||
|
||||
// ApplyStatus mocks base method.
|
||||
func (m *MockResourceInterface) ApplyStatus(arg0 context.Context, arg1 string, arg2 *unstructured.Unstructured, arg3 v1.ApplyOptions) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ApplyStatus", arg0, arg1, arg2, arg3)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ApplyStatus indicates an expected call of ApplyStatus.
|
||||
func (mr *MockResourceInterfaceMockRecorder) ApplyStatus(arg0, arg1, arg2, arg3 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyStatus", reflect.TypeOf((*MockResourceInterface)(nil).ApplyStatus), arg0, arg1, arg2, arg3)
|
||||
}
|
||||
|
||||
// Create mocks base method.
|
||||
func (m *MockResourceInterface) Create(arg0 context.Context, arg1 *unstructured.Unstructured, arg2 v1.CreateOptions, arg3 ...string) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2}
|
||||
for _, a := range arg3 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Create", varargs...)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Create indicates an expected call of Create.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Create(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1, arg2}, arg3...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockResourceInterface)(nil).Create), varargs...)
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockResourceInterface) Delete(arg0 context.Context, arg1 string, arg2 v1.DeleteOptions, arg3 ...string) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2}
|
||||
for _, a := range arg3 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Delete", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Delete(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1, arg2}, arg3...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockResourceInterface)(nil).Delete), varargs...)
|
||||
}
|
||||
|
||||
// DeleteCollection mocks base method.
|
||||
func (m *MockResourceInterface) DeleteCollection(arg0 context.Context, arg1 v1.DeleteOptions, arg2 v1.ListOptions) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteCollection", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteCollection indicates an expected call of DeleteCollection.
|
||||
func (mr *MockResourceInterfaceMockRecorder) DeleteCollection(arg0, arg1, arg2 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCollection", reflect.TypeOf((*MockResourceInterface)(nil).DeleteCollection), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Get mocks base method.
|
||||
func (m *MockResourceInterface) Get(arg0 context.Context, arg1 string, arg2 v1.GetOptions, arg3 ...string) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2}
|
||||
for _, a := range arg3 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Get", varargs...)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Get(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1, arg2}, arg3...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockResourceInterface)(nil).Get), varargs...)
|
||||
}
|
||||
|
||||
// List mocks base method.
|
||||
func (m *MockResourceInterface) List(arg0 context.Context, arg1 v1.ListOptions) (*unstructured.UnstructuredList, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "List", arg0, arg1)
|
||||
ret0, _ := ret[0].(*unstructured.UnstructuredList)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// List indicates an expected call of List.
|
||||
func (mr *MockResourceInterfaceMockRecorder) List(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockResourceInterface)(nil).List), arg0, arg1)
|
||||
}
|
||||
|
||||
// Patch mocks base method.
|
||||
func (m *MockResourceInterface) Patch(arg0 context.Context, arg1 string, arg2 types.PatchType, arg3 []byte, arg4 v1.PatchOptions, arg5 ...string) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2, arg3, arg4}
|
||||
for _, a := range arg5 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Patch", varargs...)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Patch indicates an expected call of Patch.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Patch(arg0, arg1, arg2, arg3, arg4 any, arg5 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1, arg2, arg3, arg4}, arg5...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockResourceInterface)(nil).Patch), varargs...)
|
||||
}
|
||||
|
||||
// Update mocks base method.
|
||||
func (m *MockResourceInterface) Update(arg0 context.Context, arg1 *unstructured.Unstructured, arg2 v1.UpdateOptions, arg3 ...string) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2}
|
||||
for _, a := range arg3 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Update", varargs...)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Update indicates an expected call of Update.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Update(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1, arg2}, arg3...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockResourceInterface)(nil).Update), varargs...)
|
||||
}
|
||||
|
||||
// UpdateStatus mocks base method.
|
||||
func (m *MockResourceInterface) UpdateStatus(arg0 context.Context, arg1 *unstructured.Unstructured, arg2 v1.UpdateOptions) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateStatus", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateStatus indicates an expected call of UpdateStatus.
|
||||
func (mr *MockResourceInterfaceMockRecorder) UpdateStatus(arg0, arg1, arg2 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockResourceInterface)(nil).UpdateStatus), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Watch mocks base method.
|
||||
func (m *MockResourceInterface) Watch(arg0 context.Context, arg1 v1.ListOptions) (watch.Interface, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Watch", arg0, arg1)
|
||||
ret0, _ := ret[0].(watch.Interface)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Watch indicates an expected call of Watch.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Watch(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockResourceInterface)(nil).Watch), arg0, arg1)
|
||||
}
|
121
pkg/sqlcache/informer/factory/db_mocks_test.go
Normal file
121
pkg/sqlcache/informer/factory/db_mocks_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/rancher/steve/pkg/sqlcache/db (interfaces: TXClient)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen --build_flags=--mod=mod -package factory -destination ./db_mocks_test.go github.com/rancher/steve/pkg/sqlcache/db TXClient
|
||||
//
|
||||
|
||||
// Package factory is a generated GoMock package.
|
||||
package factory
|
||||
|
||||
import (
|
||||
sql "database/sql"
|
||||
reflect "reflect"
|
||||
|
||||
transaction "github.com/rancher/steve/pkg/sqlcache/db/transaction"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockTXClient is a mock of TXClient interface.
|
||||
type MockTXClient struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockTXClientMockRecorder
|
||||
}
|
||||
|
||||
// MockTXClientMockRecorder is the mock recorder for MockTXClient.
|
||||
type MockTXClientMockRecorder struct {
|
||||
mock *MockTXClient
|
||||
}
|
||||
|
||||
// NewMockTXClient creates a new mock instance.
|
||||
func NewMockTXClient(ctrl *gomock.Controller) *MockTXClient {
|
||||
mock := &MockTXClient{ctrl: ctrl}
|
||||
mock.recorder = &MockTXClientMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockTXClient) EXPECT() *MockTXClientMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Cancel mocks base method.
|
||||
func (m *MockTXClient) Cancel() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Cancel")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Cancel indicates an expected call of Cancel.
|
||||
func (mr *MockTXClientMockRecorder) Cancel() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cancel", reflect.TypeOf((*MockTXClient)(nil).Cancel))
|
||||
}
|
||||
|
||||
// Commit mocks base method.
|
||||
func (m *MockTXClient) Commit() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Commit")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Commit indicates an expected call of Commit.
|
||||
func (mr *MockTXClientMockRecorder) Commit() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockTXClient)(nil).Commit))
|
||||
}
|
||||
|
||||
// Exec mocks base method.
|
||||
func (m *MockTXClient) Exec(arg0 string, arg1 ...any) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Exec", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Exec indicates an expected call of Exec.
|
||||
func (mr *MockTXClientMockRecorder) Exec(arg0 any, arg1 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockTXClient)(nil).Exec), varargs...)
|
||||
}
|
||||
|
||||
// Stmt mocks base method.
|
||||
func (m *MockTXClient) Stmt(arg0 *sql.Stmt) transaction.Stmt {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Stmt", arg0)
|
||||
ret0, _ := ret[0].(transaction.Stmt)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Stmt indicates an expected call of Stmt.
|
||||
func (mr *MockTXClientMockRecorder) Stmt(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stmt", reflect.TypeOf((*MockTXClient)(nil).Stmt), arg0)
|
||||
}
|
||||
|
||||
// StmtExec mocks base method.
|
||||
func (m *MockTXClient) StmtExec(arg0 transaction.Stmt, arg1 ...any) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "StmtExec", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// StmtExec indicates an expected call of StmtExec.
|
||||
func (mr *MockTXClientMockRecorder) StmtExec(arg0 any, arg1 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StmtExec", reflect.TypeOf((*MockTXClient)(nil).StmtExec), varargs...)
|
||||
}
|
237
pkg/sqlcache/informer/factory/dynamic_mocks_test.go
Normal file
237
pkg/sqlcache/informer/factory/dynamic_mocks_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: k8s.io/client-go/dynamic (interfaces: ResourceInterface)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen --build_flags=--mod=mod -package factory -destination ./dynamic_mocks_test.go k8s.io/client-go/dynamic ResourceInterface
|
||||
//
|
||||
|
||||
// Package factory is a generated GoMock package.
|
||||
package factory
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
types "k8s.io/apimachinery/pkg/types"
|
||||
watch "k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
// MockResourceInterface is a mock of ResourceInterface interface.
|
||||
type MockResourceInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockResourceInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockResourceInterfaceMockRecorder is the mock recorder for MockResourceInterface.
|
||||
type MockResourceInterfaceMockRecorder struct {
|
||||
mock *MockResourceInterface
|
||||
}
|
||||
|
||||
// NewMockResourceInterface creates a new mock instance.
|
||||
func NewMockResourceInterface(ctrl *gomock.Controller) *MockResourceInterface {
|
||||
mock := &MockResourceInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockResourceInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockResourceInterface) EXPECT() *MockResourceInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Apply mocks base method.
|
||||
func (m *MockResourceInterface) Apply(arg0 context.Context, arg1 string, arg2 *unstructured.Unstructured, arg3 v1.ApplyOptions, arg4 ...string) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2, arg3}
|
||||
for _, a := range arg4 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Apply", varargs...)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Apply indicates an expected call of Apply.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Apply(arg0, arg1, arg2, arg3 any, arg4 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1, arg2, arg3}, arg4...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockResourceInterface)(nil).Apply), varargs...)
|
||||
}
|
||||
|
||||
// ApplyStatus mocks base method.
|
||||
func (m *MockResourceInterface) ApplyStatus(arg0 context.Context, arg1 string, arg2 *unstructured.Unstructured, arg3 v1.ApplyOptions) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ApplyStatus", arg0, arg1, arg2, arg3)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ApplyStatus indicates an expected call of ApplyStatus.
|
||||
func (mr *MockResourceInterfaceMockRecorder) ApplyStatus(arg0, arg1, arg2, arg3 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyStatus", reflect.TypeOf((*MockResourceInterface)(nil).ApplyStatus), arg0, arg1, arg2, arg3)
|
||||
}
|
||||
|
||||
// Create mocks base method.
|
||||
func (m *MockResourceInterface) Create(arg0 context.Context, arg1 *unstructured.Unstructured, arg2 v1.CreateOptions, arg3 ...string) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2}
|
||||
for _, a := range arg3 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Create", varargs...)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Create indicates an expected call of Create.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Create(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1, arg2}, arg3...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockResourceInterface)(nil).Create), varargs...)
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockResourceInterface) Delete(arg0 context.Context, arg1 string, arg2 v1.DeleteOptions, arg3 ...string) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2}
|
||||
for _, a := range arg3 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Delete", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Delete(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1, arg2}, arg3...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockResourceInterface)(nil).Delete), varargs...)
|
||||
}
|
||||
|
||||
// DeleteCollection mocks base method.
|
||||
func (m *MockResourceInterface) DeleteCollection(arg0 context.Context, arg1 v1.DeleteOptions, arg2 v1.ListOptions) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteCollection", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteCollection indicates an expected call of DeleteCollection.
|
||||
func (mr *MockResourceInterfaceMockRecorder) DeleteCollection(arg0, arg1, arg2 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCollection", reflect.TypeOf((*MockResourceInterface)(nil).DeleteCollection), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Get mocks base method.
|
||||
func (m *MockResourceInterface) Get(arg0 context.Context, arg1 string, arg2 v1.GetOptions, arg3 ...string) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2}
|
||||
for _, a := range arg3 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Get", varargs...)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Get(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1, arg2}, arg3...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockResourceInterface)(nil).Get), varargs...)
|
||||
}
|
||||
|
||||
// List mocks base method.
|
||||
func (m *MockResourceInterface) List(arg0 context.Context, arg1 v1.ListOptions) (*unstructured.UnstructuredList, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "List", arg0, arg1)
|
||||
ret0, _ := ret[0].(*unstructured.UnstructuredList)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// List indicates an expected call of List.
|
||||
func (mr *MockResourceInterfaceMockRecorder) List(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockResourceInterface)(nil).List), arg0, arg1)
|
||||
}
|
||||
|
||||
// Patch mocks base method.
|
||||
func (m *MockResourceInterface) Patch(arg0 context.Context, arg1 string, arg2 types.PatchType, arg3 []byte, arg4 v1.PatchOptions, arg5 ...string) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2, arg3, arg4}
|
||||
for _, a := range arg5 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Patch", varargs...)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Patch indicates an expected call of Patch.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Patch(arg0, arg1, arg2, arg3, arg4 any, arg5 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1, arg2, arg3, arg4}, arg5...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockResourceInterface)(nil).Patch), varargs...)
|
||||
}
|
||||
|
||||
// Update mocks base method.
|
||||
func (m *MockResourceInterface) Update(arg0 context.Context, arg1 *unstructured.Unstructured, arg2 v1.UpdateOptions, arg3 ...string) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2}
|
||||
for _, a := range arg3 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Update", varargs...)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Update indicates an expected call of Update.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Update(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1, arg2}, arg3...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockResourceInterface)(nil).Update), varargs...)
|
||||
}
|
||||
|
||||
// UpdateStatus mocks base method.
|
||||
func (m *MockResourceInterface) UpdateStatus(arg0 context.Context, arg1 *unstructured.Unstructured, arg2 v1.UpdateOptions) (*unstructured.Unstructured, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateStatus", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*unstructured.Unstructured)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateStatus indicates an expected call of UpdateStatus.
|
||||
func (mr *MockResourceInterfaceMockRecorder) UpdateStatus(arg0, arg1, arg2 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockResourceInterface)(nil).UpdateStatus), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Watch mocks base method.
|
||||
func (m *MockResourceInterface) Watch(arg0 context.Context, arg1 v1.ListOptions) (watch.Interface, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Watch", arg0, arg1)
|
||||
ret0, _ := ret[0].(watch.Interface)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Watch indicates an expected call of Watch.
|
||||
func (mr *MockResourceInterfaceMockRecorder) Watch(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockResourceInterface)(nil).Watch), arg0, arg1)
|
||||
}
|
179
pkg/sqlcache/informer/factory/factory_mocks_test.go
Normal file
179
pkg/sqlcache/informer/factory/factory_mocks_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/rancher/steve/pkg/sqlcache/informer/factory (interfaces: DBClient)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen --build_flags=--mod=mod -package factory -destination ./factory_mocks_test.go github.com/rancher/steve/pkg/sqlcache/informer/factory DBClient
|
||||
//
|
||||
|
||||
// Package factory is a generated GoMock package.
|
||||
package factory
|
||||
|
||||
import (
|
||||
context "context"
|
||||
sql "database/sql"
|
||||
reflect "reflect"
|
||||
|
||||
db "github.com/rancher/steve/pkg/sqlcache/db"
|
||||
transaction "github.com/rancher/steve/pkg/sqlcache/db/transaction"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockDBClient is a mock of DBClient interface.
|
||||
type MockDBClient struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockDBClientMockRecorder
|
||||
}
|
||||
|
||||
// MockDBClientMockRecorder is the mock recorder for MockDBClient.
|
||||
type MockDBClientMockRecorder struct {
|
||||
mock *MockDBClient
|
||||
}
|
||||
|
||||
// NewMockDBClient creates a new mock instance.
|
||||
func NewMockDBClient(ctrl *gomock.Controller) *MockDBClient {
|
||||
mock := &MockDBClient{ctrl: ctrl}
|
||||
mock.recorder = &MockDBClientMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockDBClient) EXPECT() *MockDBClientMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// BeginTx mocks base method.
|
||||
func (m *MockDBClient) BeginTx(arg0 context.Context, arg1 bool) (db.TXClient, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "BeginTx", arg0, arg1)
|
||||
ret0, _ := ret[0].(db.TXClient)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// BeginTx indicates an expected call of BeginTx.
|
||||
func (mr *MockDBClientMockRecorder) BeginTx(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTx", reflect.TypeOf((*MockDBClient)(nil).BeginTx), arg0, arg1)
|
||||
}
|
||||
|
||||
// CloseStmt mocks base method.
|
||||
func (m *MockDBClient) CloseStmt(arg0 db.Closable) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CloseStmt", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CloseStmt indicates an expected call of CloseStmt.
|
||||
func (mr *MockDBClientMockRecorder) CloseStmt(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseStmt", reflect.TypeOf((*MockDBClient)(nil).CloseStmt), arg0)
|
||||
}
|
||||
|
||||
// NewConnection mocks base method.
|
||||
func (m *MockDBClient) NewConnection() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "NewConnection")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// NewConnection indicates an expected call of NewConnection.
|
||||
func (mr *MockDBClientMockRecorder) NewConnection() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewConnection", reflect.TypeOf((*MockDBClient)(nil).NewConnection))
|
||||
}
|
||||
|
||||
// Prepare mocks base method.
|
||||
func (m *MockDBClient) Prepare(arg0 string) *sql.Stmt {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Prepare", arg0)
|
||||
ret0, _ := ret[0].(*sql.Stmt)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Prepare indicates an expected call of Prepare.
|
||||
func (mr *MockDBClientMockRecorder) Prepare(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prepare", reflect.TypeOf((*MockDBClient)(nil).Prepare), arg0)
|
||||
}
|
||||
|
||||
// QueryForRows mocks base method.
|
||||
func (m *MockDBClient) QueryForRows(arg0 context.Context, arg1 transaction.Stmt, arg2 ...any) (*sql.Rows, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1}
|
||||
for _, a := range arg2 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "QueryForRows", varargs...)
|
||||
ret0, _ := ret[0].(*sql.Rows)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// QueryForRows indicates an expected call of QueryForRows.
|
||||
func (mr *MockDBClientMockRecorder) QueryForRows(arg0, arg1 any, arg2 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1}, arg2...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryForRows", reflect.TypeOf((*MockDBClient)(nil).QueryForRows), varargs...)
|
||||
}
|
||||
|
||||
// ReadInt mocks base method.
|
||||
func (m *MockDBClient) ReadInt(arg0 db.Rows) (int, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadInt", arg0)
|
||||
ret0, _ := ret[0].(int)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadInt indicates an expected call of ReadInt.
|
||||
func (mr *MockDBClientMockRecorder) ReadInt(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadInt", reflect.TypeOf((*MockDBClient)(nil).ReadInt), arg0)
|
||||
}
|
||||
|
||||
// ReadObjects mocks base method.
|
||||
func (m *MockDBClient) ReadObjects(arg0 db.Rows, arg1 reflect.Type, arg2 bool) ([]any, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadObjects", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].([]any)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadObjects indicates an expected call of ReadObjects.
|
||||
func (mr *MockDBClientMockRecorder) ReadObjects(arg0, arg1, arg2 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadObjects", reflect.TypeOf((*MockDBClient)(nil).ReadObjects), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// ReadStrings mocks base method.
|
||||
func (m *MockDBClient) ReadStrings(arg0 db.Rows) ([]string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadStrings", arg0)
|
||||
ret0, _ := ret[0].([]string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadStrings indicates an expected call of ReadStrings.
|
||||
func (mr *MockDBClientMockRecorder) ReadStrings(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadStrings", reflect.TypeOf((*MockDBClient)(nil).ReadStrings), arg0)
|
||||
}
|
||||
|
||||
// Upsert mocks base method.
|
||||
func (m *MockDBClient) Upsert(arg0 db.TXClient, arg1 *sql.Stmt, arg2 string, arg3 any, arg4 bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Upsert", arg0, arg1, arg2, arg3, arg4)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Upsert indicates an expected call of Upsert.
|
||||
func (mr *MockDBClientMockRecorder) Upsert(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upsert", reflect.TypeOf((*MockDBClient)(nil).Upsert), arg0, arg1, arg2, arg3, arg4)
|
||||
}
|
186
pkg/sqlcache/informer/factory/informer_factory.go
Normal file
186
pkg/sqlcache/informer/factory/informer_factory.go
Normal file
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
Package factory provides a cache factory for the sql-based cache.
|
||||
*/
|
||||
package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/lasso/pkg/log"
|
||||
"github.com/rancher/steve/pkg/sqlcache/db"
|
||||
"github.com/rancher/steve/pkg/sqlcache/encryption"
|
||||
"github.com/rancher/steve/pkg/sqlcache/informer"
|
||||
sqlStore "github.com/rancher/steve/pkg/sqlcache/store"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
// EncryptAllEnvVar is set to "true" if users want all types' data blobs to be encrypted in SQLite
|
||||
// otherwise only variables in defaultEncryptedResourceTypes will have their blobs encrypted
|
||||
const EncryptAllEnvVar = "CATTLE_ENCRYPT_CACHE_ALL"
|
||||
|
||||
// CacheFactory builds Informer instances and keeps a cache of instances it created
|
||||
type CacheFactory struct {
|
||||
wg wait.Group
|
||||
dbClient DBClient
|
||||
stopCh chan struct{}
|
||||
mutex sync.RWMutex
|
||||
encryptAll bool
|
||||
|
||||
newInformer newInformer
|
||||
|
||||
informers map[schema.GroupVersionKind]*guardedInformer
|
||||
informersMutex sync.Mutex
|
||||
}
|
||||
|
||||
type guardedInformer struct {
|
||||
informer *informer.Informer
|
||||
mutex *sync.Mutex
|
||||
}
|
||||
|
||||
type newInformer func(client dynamic.ResourceInterface, fields [][]string, transform cache.TransformFunc, gvk schema.GroupVersionKind, db sqlStore.DBClient, shouldEncrypt bool, namespace bool) (*informer.Informer, error)
|
||||
|
||||
type DBClient interface {
|
||||
informer.DBClient
|
||||
sqlStore.DBClient
|
||||
connector
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
informer.ByOptionsLister
|
||||
}
|
||||
|
||||
type connector interface {
|
||||
NewConnection() error
|
||||
}
|
||||
|
||||
var defaultEncryptedResourceTypes = map[schema.GroupVersionKind]struct{}{
|
||||
{
|
||||
Version: "v1",
|
||||
Kind: "Secret",
|
||||
}: {},
|
||||
}
|
||||
|
||||
// NewCacheFactory returns an informer factory instance
|
||||
func NewCacheFactory() (*CacheFactory, error) {
|
||||
m, err := encryption.NewManager()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbClient, err := db.NewClient(nil, m, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CacheFactory{
|
||||
wg: wait.Group{},
|
||||
stopCh: make(chan struct{}),
|
||||
encryptAll: os.Getenv(EncryptAllEnvVar) == "true",
|
||||
dbClient: dbClient,
|
||||
newInformer: informer.NewInformer,
|
||||
informers: map[schema.GroupVersionKind]*guardedInformer{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CacheFor returns an informer for given GVK, using sql store indexed with fields, using the specified client. For virtual fields, they must be added by the transform function
|
||||
// and specified by fields to be used for later fields.
|
||||
func (f *CacheFactory) CacheFor(fields [][]string, transform cache.TransformFunc, client dynamic.ResourceInterface, gvk schema.GroupVersionKind, namespaced bool, watchable bool) (Cache, error) {
|
||||
// First of all block Reset() until we are done
|
||||
f.mutex.RLock()
|
||||
defer f.mutex.RUnlock()
|
||||
|
||||
// Second, check if the informer and its accompanying informer-specific mutex exist already in the informers cache
|
||||
// If not, start by creating such informer-specific mutex. That is used later to ensure no two goroutines create
|
||||
// informers for the same GVK at the same type
|
||||
f.informersMutex.Lock()
|
||||
// Note: the informers cache is protected by informersMutex, which we don't want to hold for very long because
|
||||
// that blocks CacheFor for other GVKs, hence not deferring unlock here
|
||||
gi, ok := f.informers[gvk]
|
||||
if !ok {
|
||||
gi = &guardedInformer{
|
||||
informer: nil,
|
||||
mutex: &sync.Mutex{},
|
||||
}
|
||||
f.informers[gvk] = gi
|
||||
}
|
||||
f.informersMutex.Unlock()
|
||||
|
||||
// At this point an informer-specific mutex (gi.mutex) is guaranteed to exist. Lock it
|
||||
gi.mutex.Lock()
|
||||
defer gi.mutex.Unlock()
|
||||
|
||||
// Then: if the informer really was not created yet (first time here or previous times have errored out)
|
||||
// actually create the informer
|
||||
if gi.informer == nil {
|
||||
start := time.Now()
|
||||
log.Debugf("CacheFor STARTS creating informer for %v", gvk)
|
||||
defer func() {
|
||||
log.Debugf("CacheFor IS DONE creating informer for %v (took %v)", gvk, time.Now().Sub(start))
|
||||
}()
|
||||
|
||||
_, encryptResourceAlways := defaultEncryptedResourceTypes[gvk]
|
||||
shouldEncrypt := f.encryptAll || encryptResourceAlways
|
||||
i, err := f.newInformer(client, fields, transform, gvk, f.dbClient, shouldEncrypt, namespaced)
|
||||
if err != nil {
|
||||
return Cache{}, err
|
||||
}
|
||||
|
||||
err = i.SetWatchErrorHandler(func(r *cache.Reflector, err error) {
|
||||
if !watchable && errors.IsMethodNotSupported(err) {
|
||||
// expected, continue without logging
|
||||
return
|
||||
}
|
||||
cache.DefaultWatchErrorHandler(r, err)
|
||||
})
|
||||
if err != nil {
|
||||
return Cache{}, err
|
||||
}
|
||||
|
||||
f.wg.StartWithChannel(f.stopCh, i.Run)
|
||||
|
||||
gi.informer = i
|
||||
}
|
||||
|
||||
if !cache.WaitForCacheSync(f.stopCh, gi.informer.HasSynced) {
|
||||
return Cache{}, fmt.Errorf("failed to sync SQLite Informer cache for GVK %v", gvk)
|
||||
}
|
||||
|
||||
// At this point the informer is ready, return it
|
||||
return Cache{ByOptionsLister: gi.informer}, nil
|
||||
}
|
||||
|
||||
// Reset closes the stopCh which stops any running informers, assigns a new stopCh, resets the GVK-informer cache, and resets
|
||||
// the database connection which wipes any current sqlite database at the default location.
|
||||
func (f *CacheFactory) Reset() error {
|
||||
if f.dbClient == nil {
|
||||
// nothing to reset
|
||||
return nil
|
||||
}
|
||||
|
||||
// first of all wait until all CacheFor() calls that create new informers are finished. Also block any new ones
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
// now that we are alone, stop all informers created until this point
|
||||
close(f.stopCh)
|
||||
f.stopCh = make(chan struct{})
|
||||
f.wg.Wait()
|
||||
|
||||
// and get rid of all references to those informers and their mutexes
|
||||
f.informersMutex.Lock()
|
||||
defer f.informersMutex.Unlock()
|
||||
f.informers = make(map[schema.GroupVersionKind]*guardedInformer)
|
||||
|
||||
// finally, reset the DB connection
|
||||
err := f.dbClient.NewConnection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
287
pkg/sqlcache/informer/factory/informer_factory_test.go
Normal file
287
pkg/sqlcache/informer/factory/informer_factory_test.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/steve/pkg/sqlcache/informer"
|
||||
|
||||
sqlStore "github.com/rancher/steve/pkg/sqlcache/store"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
//go:generate mockgen --build_flags=--mod=mod -package factory -destination ./factory_mocks_test.go github.com/rancher/steve/pkg/sqlcache/informer/factory DBClient
|
||||
//go:generate mockgen --build_flags=--mod=mod -package factory -destination ./db_mocks_test.go github.com/rancher/steve/pkg/sqlcache/db TXClient
|
||||
//go:generate mockgen --build_flags=--mod=mod -package factory -destination ./dynamic_mocks_test.go k8s.io/client-go/dynamic ResourceInterface
|
||||
//go:generate mockgen --build_flags=--mod=mod -package factory -destination ./k8s_cache_mocks_test.go k8s.io/client-go/tools/cache SharedIndexInformer
|
||||
|
||||
func TestNewCacheFactory(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
test func(t *testing.T)
|
||||
}
|
||||
|
||||
var tests []testCase
|
||||
|
||||
tests = append(tests, testCase{description: "NewCacheFactory() with no errors returned, should return no errors", test: func(t *testing.T) {
|
||||
f, err := NewCacheFactory()
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, f.dbClient)
|
||||
assert.False(t, f.encryptAll)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewCacheFactory() with no errors returned and EncryptAllEnvVar set to true, should return no errors and have encryptAll set to true", test: func(t *testing.T) {
|
||||
err := os.Setenv(EncryptAllEnvVar, "true")
|
||||
assert.Nil(t, err)
|
||||
f, err := NewCacheFactory()
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, f.dbClient)
|
||||
assert.True(t, f.encryptAll)
|
||||
}})
|
||||
// cannot run as parallel because tests involve changing env var
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheFor(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
test func(t *testing.T)
|
||||
}
|
||||
|
||||
var tests []testCase
|
||||
|
||||
tests = append(tests, testCase{description: "CacheFor() with no errors returned, HasSync returning true, and stopCh not closed, should return no error and should call Informer.Run(). A subsequent call to CacheFor() should return same informer", test: func(t *testing.T) {
|
||||
dbClient := NewMockDBClient(gomock.NewController(t))
|
||||
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
||||
fields := [][]string{{"something"}}
|
||||
expectedGVK := schema.GroupVersionKind{}
|
||||
sii := NewMockSharedIndexInformer(gomock.NewController(t))
|
||||
sii.EXPECT().HasSynced().Return(true).AnyTimes()
|
||||
sii.EXPECT().Run(gomock.Any()).MinTimes(1)
|
||||
sii.EXPECT().SetWatchErrorHandler(gomock.Any())
|
||||
i := &informer.Informer{
|
||||
// need to set this so Run function is not nil
|
||||
SharedIndexInformer: sii,
|
||||
}
|
||||
expectedC := Cache{
|
||||
ByOptionsLister: i,
|
||||
}
|
||||
testNewInformer := func(client dynamic.ResourceInterface, fields [][]string, transform cache.TransformFunc, gvk schema.GroupVersionKind, db sqlStore.DBClient, shouldEncrypt bool, namespaced bool) (*informer.Informer, error) {
|
||||
assert.Equal(t, client, dynamicClient)
|
||||
assert.Equal(t, fields, fields)
|
||||
assert.Equal(t, expectedGVK, gvk)
|
||||
assert.Equal(t, db, dbClient)
|
||||
assert.Equal(t, false, shouldEncrypt)
|
||||
return i, nil
|
||||
}
|
||||
f := &CacheFactory{
|
||||
dbClient: dbClient,
|
||||
stopCh: make(chan struct{}),
|
||||
newInformer: testNewInformer,
|
||||
informers: map[schema.GroupVersionKind]*guardedInformer{},
|
||||
}
|
||||
|
||||
go func() {
|
||||
// this function ensures that stopCh is open for the duration of this test but if part of a longer process it will be closed eventually
|
||||
time.Sleep(5 * time.Second)
|
||||
close(f.stopCh)
|
||||
}()
|
||||
var c Cache
|
||||
var err error
|
||||
c, err = f.CacheFor(fields, nil, dynamicClient, expectedGVK, false, true)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expectedC, c)
|
||||
// this sleep is critical to the test. It ensure there has been enough time for expected function like Run to be invoked in their go routines.
|
||||
time.Sleep(1 * time.Second)
|
||||
c2, err := f.CacheFor(fields, nil, dynamicClient, expectedGVK, false, true)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, c, c2)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "CacheFor() with no errors returned, HasSync returning false, and stopCh not closed, should call Run() and return an error", test: func(t *testing.T) {
|
||||
dbClient := NewMockDBClient(gomock.NewController(t))
|
||||
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
||||
fields := [][]string{{"something"}}
|
||||
expectedGVK := schema.GroupVersionKind{}
|
||||
|
||||
sii := NewMockSharedIndexInformer(gomock.NewController(t))
|
||||
sii.EXPECT().HasSynced().Return(false).AnyTimes()
|
||||
sii.EXPECT().Run(gomock.Any())
|
||||
sii.EXPECT().SetWatchErrorHandler(gomock.Any())
|
||||
expectedI := &informer.Informer{
|
||||
// need to set this so Run function is not nil
|
||||
SharedIndexInformer: sii,
|
||||
}
|
||||
testNewInformer := func(client dynamic.ResourceInterface, fields [][]string, transform cache.TransformFunc, gvk schema.GroupVersionKind, db sqlStore.DBClient, shouldEncrypt, namespaced bool) (*informer.Informer, error) {
|
||||
assert.Equal(t, client, dynamicClient)
|
||||
assert.Equal(t, fields, fields)
|
||||
assert.Equal(t, expectedGVK, gvk)
|
||||
assert.Equal(t, db, dbClient)
|
||||
assert.Equal(t, false, shouldEncrypt)
|
||||
return expectedI, nil
|
||||
}
|
||||
f := &CacheFactory{
|
||||
dbClient: dbClient,
|
||||
stopCh: make(chan struct{}),
|
||||
newInformer: testNewInformer,
|
||||
informers: map[schema.GroupVersionKind]*guardedInformer{},
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
close(f.stopCh)
|
||||
}()
|
||||
var err error
|
||||
_, err = f.CacheFor(fields, nil, dynamicClient, expectedGVK, false, true)
|
||||
assert.NotNil(t, err)
|
||||
time.Sleep(2 * time.Second)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "CacheFor() with no errors returned, HasSync returning true, and stopCh closed, should not call Run() more than once and not return an error", test: func(t *testing.T) {
|
||||
dbClient := NewMockDBClient(gomock.NewController(t))
|
||||
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
||||
fields := [][]string{{"something"}}
|
||||
expectedGVK := schema.GroupVersionKind{}
|
||||
|
||||
sii := NewMockSharedIndexInformer(gomock.NewController(t))
|
||||
sii.EXPECT().HasSynced().Return(true).AnyTimes()
|
||||
// may or may not call run initially
|
||||
sii.EXPECT().Run(gomock.Any()).MaxTimes(1)
|
||||
sii.EXPECT().SetWatchErrorHandler(gomock.Any())
|
||||
i := &informer.Informer{
|
||||
// need to set this so Run function is not nil
|
||||
SharedIndexInformer: sii,
|
||||
}
|
||||
expectedC := Cache{
|
||||
ByOptionsLister: i,
|
||||
}
|
||||
testNewInformer := func(client dynamic.ResourceInterface, fields [][]string, transform cache.TransformFunc, gvk schema.GroupVersionKind, db sqlStore.DBClient, shouldEncrypt, namespaced bool) (*informer.Informer, error) {
|
||||
assert.Equal(t, client, dynamicClient)
|
||||
assert.Equal(t, fields, fields)
|
||||
assert.Equal(t, expectedGVK, gvk)
|
||||
assert.Equal(t, db, dbClient)
|
||||
assert.Equal(t, false, shouldEncrypt)
|
||||
return i, nil
|
||||
}
|
||||
f := &CacheFactory{
|
||||
dbClient: dbClient,
|
||||
stopCh: make(chan struct{}),
|
||||
newInformer: testNewInformer,
|
||||
informers: map[schema.GroupVersionKind]*guardedInformer{},
|
||||
}
|
||||
|
||||
close(f.stopCh)
|
||||
var c Cache
|
||||
var err error
|
||||
c, err = f.CacheFor(fields, nil, dynamicClient, expectedGVK, false, true)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expectedC, c)
|
||||
time.Sleep(1 * time.Second)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "CacheFor() with no errors returned and encryptAll set to true, should return no error and pass shouldEncrypt as true to newInformer func", test: func(t *testing.T) {
|
||||
dbClient := NewMockDBClient(gomock.NewController(t))
|
||||
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
||||
fields := [][]string{{"something"}}
|
||||
expectedGVK := schema.GroupVersionKind{}
|
||||
sii := NewMockSharedIndexInformer(gomock.NewController(t))
|
||||
sii.EXPECT().HasSynced().Return(true)
|
||||
sii.EXPECT().Run(gomock.Any()).MinTimes(1).AnyTimes()
|
||||
sii.EXPECT().SetWatchErrorHandler(gomock.Any())
|
||||
i := &informer.Informer{
|
||||
// need to set this so Run function is not nil
|
||||
SharedIndexInformer: sii,
|
||||
}
|
||||
expectedC := Cache{
|
||||
ByOptionsLister: i,
|
||||
}
|
||||
testNewInformer := func(client dynamic.ResourceInterface, fields [][]string, transform cache.TransformFunc, gvk schema.GroupVersionKind, db sqlStore.DBClient, shouldEncrypt, namespaced bool) (*informer.Informer, error) {
|
||||
assert.Equal(t, client, dynamicClient)
|
||||
assert.Equal(t, fields, fields)
|
||||
assert.Equal(t, expectedGVK, gvk)
|
||||
assert.Equal(t, db, dbClient)
|
||||
assert.Equal(t, true, shouldEncrypt)
|
||||
return i, nil
|
||||
}
|
||||
f := &CacheFactory{
|
||||
dbClient: dbClient,
|
||||
stopCh: make(chan struct{}),
|
||||
newInformer: testNewInformer,
|
||||
encryptAll: true,
|
||||
informers: map[schema.GroupVersionKind]*guardedInformer{},
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(10 * time.Second)
|
||||
close(f.stopCh)
|
||||
}()
|
||||
var c Cache
|
||||
var err error
|
||||
c, err = f.CacheFor(fields, nil, dynamicClient, expectedGVK, false, true)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expectedC, c)
|
||||
time.Sleep(1 * time.Second)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "CacheFor() with no errors returned, HasSync returning true, stopCh not closed, and transform func should return no error", test: func(t *testing.T) {
|
||||
dbClient := NewMockDBClient(gomock.NewController(t))
|
||||
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
||||
fields := [][]string{{"something"}}
|
||||
expectedGVK := schema.GroupVersionKind{}
|
||||
sii := NewMockSharedIndexInformer(gomock.NewController(t))
|
||||
sii.EXPECT().HasSynced().Return(true)
|
||||
sii.EXPECT().Run(gomock.Any()).MinTimes(1)
|
||||
sii.EXPECT().SetWatchErrorHandler(gomock.Any())
|
||||
transformFunc := func(input interface{}) (interface{}, error) {
|
||||
return "someoutput", nil
|
||||
}
|
||||
i := &informer.Informer{
|
||||
// need to set this so Run function is not nil
|
||||
SharedIndexInformer: sii,
|
||||
}
|
||||
expectedC := Cache{
|
||||
ByOptionsLister: i,
|
||||
}
|
||||
testNewInformer := func(client dynamic.ResourceInterface, fields [][]string, transform cache.TransformFunc, gvk schema.GroupVersionKind, db sqlStore.DBClient, shouldEncrypt bool, namespaced bool) (*informer.Informer, error) {
|
||||
// we can't test func == func, so instead we check if the output was as expected
|
||||
input := "someinput"
|
||||
ouput, err := transform(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)
|
||||
|
||||
assert.Equal(t, client, dynamicClient)
|
||||
assert.Equal(t, fields, fields)
|
||||
assert.Equal(t, expectedGVK, gvk)
|
||||
assert.Equal(t, db, dbClient)
|
||||
assert.Equal(t, false, shouldEncrypt)
|
||||
return i, nil
|
||||
}
|
||||
f := &CacheFactory{
|
||||
dbClient: dbClient,
|
||||
stopCh: make(chan struct{}),
|
||||
newInformer: testNewInformer,
|
||||
informers: map[schema.GroupVersionKind]*guardedInformer{},
|
||||
}
|
||||
|
||||
go func() {
|
||||
// this function ensures that stopCh is open for the duration of this test but if part of a longer process it will be closed eventually
|
||||
time.Sleep(5 * time.Second)
|
||||
close(f.stopCh)
|
||||
}()
|
||||
var c Cache
|
||||
var err error
|
||||
c, err = f.CacheFor(fields, transformFunc, dynamicClient, expectedGVK, false, true)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expectedC, c)
|
||||
time.Sleep(1 * time.Second)
|
||||
}})
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
||||
}
|
||||
}
|
223
pkg/sqlcache/informer/factory/k8s_cache_mocks_test.go
Normal file
223
pkg/sqlcache/informer/factory/k8s_cache_mocks_test.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: k8s.io/client-go/tools/cache (interfaces: SharedIndexInformer)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen --build_flags=--mod=mod -package factory -destination ./k8s_cache_mocks_test.go k8s.io/client-go/tools/cache SharedIndexInformer
|
||||
//
|
||||
|
||||
// Package factory is a generated GoMock package.
|
||||
package factory
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
cache "k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
// MockSharedIndexInformer is a mock of SharedIndexInformer interface.
|
||||
type MockSharedIndexInformer struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSharedIndexInformerMockRecorder
|
||||
}
|
||||
|
||||
// MockSharedIndexInformerMockRecorder is the mock recorder for MockSharedIndexInformer.
|
||||
type MockSharedIndexInformerMockRecorder struct {
|
||||
mock *MockSharedIndexInformer
|
||||
}
|
||||
|
||||
// NewMockSharedIndexInformer creates a new mock instance.
|
||||
func NewMockSharedIndexInformer(ctrl *gomock.Controller) *MockSharedIndexInformer {
|
||||
mock := &MockSharedIndexInformer{ctrl: ctrl}
|
||||
mock.recorder = &MockSharedIndexInformerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockSharedIndexInformer) EXPECT() *MockSharedIndexInformerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AddEventHandler mocks base method.
|
||||
func (m *MockSharedIndexInformer) AddEventHandler(arg0 cache.ResourceEventHandler) (cache.ResourceEventHandlerRegistration, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddEventHandler", arg0)
|
||||
ret0, _ := ret[0].(cache.ResourceEventHandlerRegistration)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AddEventHandler indicates an expected call of AddEventHandler.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) AddEventHandler(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEventHandler", reflect.TypeOf((*MockSharedIndexInformer)(nil).AddEventHandler), arg0)
|
||||
}
|
||||
|
||||
// AddEventHandlerWithResyncPeriod mocks base method.
|
||||
func (m *MockSharedIndexInformer) AddEventHandlerWithResyncPeriod(arg0 cache.ResourceEventHandler, arg1 time.Duration) (cache.ResourceEventHandlerRegistration, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddEventHandlerWithResyncPeriod", arg0, arg1)
|
||||
ret0, _ := ret[0].(cache.ResourceEventHandlerRegistration)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AddEventHandlerWithResyncPeriod indicates an expected call of AddEventHandlerWithResyncPeriod.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) AddEventHandlerWithResyncPeriod(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEventHandlerWithResyncPeriod", reflect.TypeOf((*MockSharedIndexInformer)(nil).AddEventHandlerWithResyncPeriod), arg0, arg1)
|
||||
}
|
||||
|
||||
// AddIndexers mocks base method.
|
||||
func (m *MockSharedIndexInformer) AddIndexers(arg0 cache.Indexers) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddIndexers", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddIndexers indicates an expected call of AddIndexers.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) AddIndexers(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddIndexers", reflect.TypeOf((*MockSharedIndexInformer)(nil).AddIndexers), arg0)
|
||||
}
|
||||
|
||||
// GetController mocks base method.
|
||||
func (m *MockSharedIndexInformer) GetController() cache.Controller {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetController")
|
||||
ret0, _ := ret[0].(cache.Controller)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetController indicates an expected call of GetController.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) GetController() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetController", reflect.TypeOf((*MockSharedIndexInformer)(nil).GetController))
|
||||
}
|
||||
|
||||
// GetIndexer mocks base method.
|
||||
func (m *MockSharedIndexInformer) GetIndexer() cache.Indexer {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetIndexer")
|
||||
ret0, _ := ret[0].(cache.Indexer)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetIndexer indicates an expected call of GetIndexer.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) GetIndexer() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIndexer", reflect.TypeOf((*MockSharedIndexInformer)(nil).GetIndexer))
|
||||
}
|
||||
|
||||
// GetStore mocks base method.
|
||||
func (m *MockSharedIndexInformer) GetStore() cache.Store {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetStore")
|
||||
ret0, _ := ret[0].(cache.Store)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetStore indicates an expected call of GetStore.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) GetStore() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStore", reflect.TypeOf((*MockSharedIndexInformer)(nil).GetStore))
|
||||
}
|
||||
|
||||
// HasSynced mocks base method.
|
||||
func (m *MockSharedIndexInformer) HasSynced() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HasSynced")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HasSynced indicates an expected call of HasSynced.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) HasSynced() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasSynced", reflect.TypeOf((*MockSharedIndexInformer)(nil).HasSynced))
|
||||
}
|
||||
|
||||
// IsStopped mocks base method.
|
||||
func (m *MockSharedIndexInformer) IsStopped() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsStopped")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsStopped indicates an expected call of IsStopped.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) IsStopped() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsStopped", reflect.TypeOf((*MockSharedIndexInformer)(nil).IsStopped))
|
||||
}
|
||||
|
||||
// LastSyncResourceVersion mocks base method.
|
||||
func (m *MockSharedIndexInformer) LastSyncResourceVersion() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LastSyncResourceVersion")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// LastSyncResourceVersion indicates an expected call of LastSyncResourceVersion.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) LastSyncResourceVersion() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastSyncResourceVersion", reflect.TypeOf((*MockSharedIndexInformer)(nil).LastSyncResourceVersion))
|
||||
}
|
||||
|
||||
// RemoveEventHandler mocks base method.
|
||||
func (m *MockSharedIndexInformer) RemoveEventHandler(arg0 cache.ResourceEventHandlerRegistration) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RemoveEventHandler", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoveEventHandler indicates an expected call of RemoveEventHandler.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) RemoveEventHandler(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveEventHandler", reflect.TypeOf((*MockSharedIndexInformer)(nil).RemoveEventHandler), arg0)
|
||||
}
|
||||
|
||||
// Run mocks base method.
|
||||
func (m *MockSharedIndexInformer) Run(arg0 <-chan struct{}) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Run", arg0)
|
||||
}
|
||||
|
||||
// Run indicates an expected call of Run.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) Run(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockSharedIndexInformer)(nil).Run), arg0)
|
||||
}
|
||||
|
||||
// SetTransform mocks base method.
|
||||
func (m *MockSharedIndexInformer) SetTransform(arg0 cache.TransformFunc) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SetTransform", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SetTransform indicates an expected call of SetTransform.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) SetTransform(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTransform", reflect.TypeOf((*MockSharedIndexInformer)(nil).SetTransform), arg0)
|
||||
}
|
||||
|
||||
// SetWatchErrorHandler mocks base method.
|
||||
func (m *MockSharedIndexInformer) SetWatchErrorHandler(arg0 cache.WatchErrorHandler) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SetWatchErrorHandler", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SetWatchErrorHandler indicates an expected call of SetWatchErrorHandler.
|
||||
func (mr *MockSharedIndexInformerMockRecorder) SetWatchErrorHandler(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWatchErrorHandler", reflect.TypeOf((*MockSharedIndexInformer)(nil).SetWatchErrorHandler), arg0)
|
||||
}
|
263
pkg/sqlcache/informer/indexer.go
Normal file
263
pkg/sqlcache/informer/indexer.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package informer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/rancher/steve/pkg/sqlcache/db"
|
||||
"github.com/rancher/steve/pkg/sqlcache/db/transaction"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
const (
|
||||
selectQueryFmt = `
|
||||
SELECT object, objectnonce, dekid FROM "%[1]s"
|
||||
WHERE key IN (
|
||||
SELECT key FROM "%[1]s_indices"
|
||||
WHERE name = ? AND value IN (?%s)
|
||||
)
|
||||
`
|
||||
createTableFmt = `CREATE TABLE IF NOT EXISTS "%[1]s_indices" (
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
key TEXT NOT NULL REFERENCES "%[1]s"(key) ON DELETE CASCADE,
|
||||
PRIMARY KEY (name, value, key)
|
||||
)`
|
||||
createIndexFmt = `CREATE INDEX IF NOT EXISTS "%[1]s_indices_index" ON "%[1]s_indices"(name, value)`
|
||||
|
||||
deleteIndicesFmt = `DELETE FROM "%s_indices" WHERE key = ?`
|
||||
addIndexFmt = `INSERT INTO "%s_indices" (name, value, key) VALUES (?, ?, ?) ON CONFLICT DO NOTHING`
|
||||
listByIndexFmt = `SELECT object, objectnonce, dekid FROM "%[1]s"
|
||||
WHERE key IN (
|
||||
SELECT key FROM "%[1]s_indices"
|
||||
WHERE name = ? AND value = ?
|
||||
)`
|
||||
listKeyByIndexFmt = `SELECT DISTINCT key FROM "%s_indices" WHERE name = ? AND value = ?`
|
||||
listIndexValuesFmt = `SELECT DISTINCT value FROM "%s_indices" WHERE name = ?`
|
||||
)
|
||||
|
||||
// Indexer is a SQLite-backed cache.Indexer which builds upon Store adding an index table
|
||||
type Indexer struct {
|
||||
Store
|
||||
indexers cache.Indexers
|
||||
indexersLock sync.RWMutex
|
||||
|
||||
deleteIndicesQuery string
|
||||
addIndexQuery string
|
||||
listByIndexQuery string
|
||||
listKeysByIndexQuery string
|
||||
listIndexValuesQuery string
|
||||
|
||||
deleteIndicesStmt *sql.Stmt
|
||||
addIndexStmt *sql.Stmt
|
||||
listByIndexStmt *sql.Stmt
|
||||
listKeysByIndexStmt *sql.Stmt
|
||||
listIndexValuesStmt *sql.Stmt
|
||||
}
|
||||
|
||||
var _ cache.Indexer = (*Indexer)(nil)
|
||||
|
||||
type Store interface {
|
||||
DBClient
|
||||
cache.Store
|
||||
|
||||
GetByKey(key string) (item any, exists bool, err error)
|
||||
GetName() string
|
||||
RegisterAfterUpsert(f func(key string, obj any, tx db.TXClient) error)
|
||||
RegisterAfterDelete(f func(key string, tx db.TXClient) error)
|
||||
GetShouldEncrypt() bool
|
||||
GetType() reflect.Type
|
||||
}
|
||||
|
||||
type DBClient interface {
|
||||
BeginTx(ctx context.Context, forWriting bool) (db.TXClient, error)
|
||||
QueryForRows(ctx context.Context, stmt transaction.Stmt, params ...any) (*sql.Rows, error)
|
||||
ReadObjects(rows db.Rows, typ reflect.Type, shouldDecrypt bool) ([]any, error)
|
||||
ReadStrings(rows db.Rows) ([]string, error)
|
||||
ReadInt(rows db.Rows) (int, error)
|
||||
Prepare(stmt string) *sql.Stmt
|
||||
CloseStmt(stmt db.Closable) error
|
||||
}
|
||||
|
||||
// NewIndexer returns a cache.Indexer backed by SQLite for objects of the given example type
|
||||
func NewIndexer(indexers cache.Indexers, s Store) (*Indexer, error) {
|
||||
tx, err := s.BeginTx(context.Background(), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
createTableQuery := fmt.Sprintf(createTableFmt, db.Sanitize(s.GetName()))
|
||||
err = tx.Exec(createTableQuery)
|
||||
if err != nil {
|
||||
return nil, &db.QueryError{QueryString: createTableQuery, Err: err}
|
||||
}
|
||||
createIndexQuery := fmt.Sprintf(createIndexFmt, db.Sanitize(s.GetName()))
|
||||
err = tx.Exec(createIndexQuery)
|
||||
if err != nil {
|
||||
return nil, &db.QueryError{QueryString: createIndexQuery, Err: err}
|
||||
}
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i := &Indexer{
|
||||
Store: s,
|
||||
indexers: indexers,
|
||||
}
|
||||
i.RegisterAfterUpsert(i.AfterUpsert)
|
||||
|
||||
i.deleteIndicesQuery = fmt.Sprintf(deleteIndicesFmt, db.Sanitize(s.GetName()))
|
||||
i.addIndexQuery = fmt.Sprintf(addIndexFmt, db.Sanitize(s.GetName()))
|
||||
i.listByIndexQuery = fmt.Sprintf(listByIndexFmt, db.Sanitize(s.GetName()))
|
||||
i.listKeysByIndexQuery = fmt.Sprintf(listKeyByIndexFmt, db.Sanitize(s.GetName()))
|
||||
i.listIndexValuesQuery = fmt.Sprintf(listIndexValuesFmt, db.Sanitize(s.GetName()))
|
||||
|
||||
i.deleteIndicesStmt = s.Prepare(i.deleteIndicesQuery)
|
||||
i.addIndexStmt = s.Prepare(i.addIndexQuery)
|
||||
i.listByIndexStmt = s.Prepare(i.listByIndexQuery)
|
||||
i.listKeysByIndexStmt = s.Prepare(i.listKeysByIndexQuery)
|
||||
i.listIndexValuesStmt = s.Prepare(i.listIndexValuesQuery)
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
/* Core methods */
|
||||
|
||||
// AfterUpsert updates indices of an object
|
||||
func (i *Indexer) AfterUpsert(key string, obj any, tx db.TXClient) error {
|
||||
// delete all
|
||||
err := tx.StmtExec(tx.Stmt(i.deleteIndicesStmt), key)
|
||||
if err != nil {
|
||||
return &db.QueryError{QueryString: i.deleteIndicesQuery, Err: err}
|
||||
}
|
||||
|
||||
// re-insert all
|
||||
i.indexersLock.RLock()
|
||||
defer i.indexersLock.RUnlock()
|
||||
for indexName, indexFunc := range i.indexers {
|
||||
values, err := indexFunc(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
err = tx.StmtExec(tx.Stmt(i.addIndexStmt), indexName, value, key)
|
||||
if err != nil {
|
||||
return &db.QueryError{QueryString: i.addIndexQuery, Err: err}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/* Satisfy cache.Indexer */
|
||||
|
||||
// Index returns a list of items that match the given object on the index function
|
||||
func (i *Indexer) Index(indexName string, obj any) ([]any, error) {
|
||||
i.indexersLock.RLock()
|
||||
defer i.indexersLock.RUnlock()
|
||||
indexFunc := i.indexers[indexName]
|
||||
if indexFunc == nil {
|
||||
return nil, fmt.Errorf("index with name %s does not exist", indexName)
|
||||
}
|
||||
|
||||
values, err := indexFunc(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// typical case
|
||||
if len(values) == 1 {
|
||||
return i.ByIndex(indexName, values[0])
|
||||
}
|
||||
|
||||
// atypical case - more than one value to lookup
|
||||
// HACK: sql.Statement.Query does not allow to pass slices in as of go 1.19 - create an ad-hoc statement
|
||||
query := fmt.Sprintf(selectQueryFmt, db.Sanitize(i.GetName()), strings.Repeat(", ?", len(values)-1))
|
||||
stmt := i.Prepare(query)
|
||||
defer i.CloseStmt(stmt)
|
||||
// HACK: Query will accept []any but not []string
|
||||
params := []any{indexName}
|
||||
for _, value := range values {
|
||||
params = append(params, value)
|
||||
}
|
||||
|
||||
rows, err := i.QueryForRows(context.TODO(), stmt, params...)
|
||||
if err != nil {
|
||||
return nil, &db.QueryError{QueryString: query, Err: err}
|
||||
}
|
||||
return i.ReadObjects(rows, i.GetType(), i.GetShouldEncrypt())
|
||||
}
|
||||
|
||||
// ByIndex returns the stored objects whose set of indexed values
|
||||
// for the named index includes the given indexed value
|
||||
func (i *Indexer) ByIndex(indexName, indexedValue string) ([]any, error) {
|
||||
rows, err := i.QueryForRows(context.TODO(), i.listByIndexStmt, indexName, indexedValue)
|
||||
if err != nil {
|
||||
return nil, &db.QueryError{QueryString: i.listByIndexQuery, Err: err}
|
||||
}
|
||||
return i.ReadObjects(rows, i.GetType(), i.GetShouldEncrypt())
|
||||
}
|
||||
|
||||
// IndexKeys returns a list of the Store keys of the objects whose indexed values in the given index include the given indexed value
|
||||
func (i *Indexer) IndexKeys(indexName, indexedValue string) ([]string, error) {
|
||||
i.indexersLock.RLock()
|
||||
defer i.indexersLock.RUnlock()
|
||||
indexFunc := i.indexers[indexName]
|
||||
if indexFunc == nil {
|
||||
return nil, fmt.Errorf("Index with name %s does not exist", indexName)
|
||||
}
|
||||
|
||||
rows, err := i.QueryForRows(context.TODO(), i.listKeysByIndexStmt, indexName, indexedValue)
|
||||
if err != nil {
|
||||
return nil, &db.QueryError{QueryString: i.listKeysByIndexQuery, Err: err}
|
||||
}
|
||||
return i.ReadStrings(rows)
|
||||
}
|
||||
|
||||
// ListIndexFuncValues wraps safeListIndexFuncValues and panics in case of I/O errors
|
||||
func (i *Indexer) ListIndexFuncValues(name string) []string {
|
||||
result, err := i.safeListIndexFuncValues(name)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unexpected error in safeListIndexFuncValues: %w", err))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// safeListIndexFuncValues returns all the indexed values of the given index
|
||||
func (i *Indexer) safeListIndexFuncValues(indexName string) ([]string, error) {
|
||||
rows, err := i.QueryForRows(context.TODO(), i.listIndexValuesStmt, indexName)
|
||||
if err != nil {
|
||||
return nil, &db.QueryError{QueryString: i.listIndexValuesQuery, Err: err}
|
||||
}
|
||||
return i.ReadStrings(rows)
|
||||
}
|
||||
|
||||
// GetIndexers returns the indexers
|
||||
func (i *Indexer) GetIndexers() cache.Indexers {
|
||||
i.indexersLock.RLock()
|
||||
defer i.indexersLock.RUnlock()
|
||||
return i.indexers
|
||||
}
|
||||
|
||||
// AddIndexers adds more indexers to this Store. If you call this after you already have data
|
||||
// in the Store, the results are undefined.
|
||||
func (i *Indexer) AddIndexers(newIndexers cache.Indexers) error {
|
||||
i.indexersLock.Lock()
|
||||
defer i.indexersLock.Unlock()
|
||||
if i.indexers == nil {
|
||||
i.indexers = make(map[string]cache.IndexFunc)
|
||||
}
|
||||
for k, v := range newIndexers {
|
||||
i.indexers[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
614
pkg/sqlcache/informer/indexer_test.go
Normal file
614
pkg/sqlcache/informer/indexer_test.go
Normal file
@@ -0,0 +1,614 @@
|
||||
/*
|
||||
Copyright 2023 SUSE LLC
|
||||
|
||||
Adapted from client-go, Copyright 2014 The Kubernetes Authors.
|
||||
*/
|
||||
|
||||
package informer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
//go:generate mockgen --build_flags=--mod=mod -package informer -destination ./sql_mocks_test.go github.com/rancher/steve/pkg/sqlcache/informer Store
|
||||
//go:generate mockgen --build_flags=--mod=mod -package informer -destination ./db_mocks_test.go github.com/rancher/steve/pkg/sqlcache/db TXClient,Rows
|
||||
|
||||
type testStoreObject struct {
|
||||
Id string
|
||||
Val string
|
||||
}
|
||||
|
||||
func TestNewIndexer(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
test func(t *testing.T)
|
||||
}
|
||||
|
||||
var tests []testCase
|
||||
|
||||
tests = append(tests, testCase{description: "NewIndexer() with no errors returned from Store or TXClient, should return no error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
client := NewMockTXClient(gomock.NewController(t))
|
||||
|
||||
objKey := "objKey"
|
||||
indexers := map[string]cache.IndexFunc{
|
||||
"a": func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
}
|
||||
storeName := "someStoreName"
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(client, nil)
|
||||
store.EXPECT().GetName().AnyTimes().Return(storeName)
|
||||
client.EXPECT().Exec(fmt.Sprintf(createTableFmt, storeName, storeName)).Return(nil)
|
||||
client.EXPECT().Exec(fmt.Sprintf(createIndexFmt, storeName, storeName)).Return(nil)
|
||||
client.EXPECT().Commit().Return(nil)
|
||||
store.EXPECT().RegisterAfterUpsert(gomock.Any())
|
||||
store.EXPECT().Prepare(fmt.Sprintf(deleteIndicesFmt, storeName))
|
||||
store.EXPECT().Prepare(fmt.Sprintf(addIndexFmt, storeName))
|
||||
store.EXPECT().Prepare(fmt.Sprintf(listByIndexFmt, storeName, storeName))
|
||||
store.EXPECT().Prepare(fmt.Sprintf(listKeyByIndexFmt, storeName))
|
||||
store.EXPECT().Prepare(fmt.Sprintf(listIndexValuesFmt, storeName))
|
||||
indexer, err := NewIndexer(indexers, store)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, cache.Indexers(indexers), indexer.indexers)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewIndexer() with Store Begin() error, should return error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
|
||||
objKey := "objKey"
|
||||
indexers := map[string]cache.IndexFunc{
|
||||
"a": func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
}
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(nil, fmt.Errorf("error"))
|
||||
_, err := NewIndexer(indexers, store)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewIndexer() with TXClient Exec() error on first call to Exec(), should return error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
client := NewMockTXClient(gomock.NewController(t))
|
||||
|
||||
objKey := "objKey"
|
||||
indexers := map[string]cache.IndexFunc{
|
||||
"a": func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
}
|
||||
storeName := "someStoreName"
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(client, nil)
|
||||
store.EXPECT().GetName().AnyTimes().Return(storeName)
|
||||
client.EXPECT().Exec(fmt.Sprintf(createTableFmt, storeName, storeName)).Return(fmt.Errorf("error"))
|
||||
_, err := NewIndexer(indexers, store)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewIndexer() with TXClient Exec() error on second call to Exec(), should return error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
client := NewMockTXClient(gomock.NewController(t))
|
||||
|
||||
objKey := "objKey"
|
||||
indexers := map[string]cache.IndexFunc{
|
||||
"a": func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
}
|
||||
storeName := "someStoreName"
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(client, nil)
|
||||
store.EXPECT().GetName().AnyTimes().Return(storeName)
|
||||
client.EXPECT().Exec(fmt.Sprintf(createTableFmt, storeName, storeName)).Return(nil)
|
||||
client.EXPECT().Exec(fmt.Sprintf(createIndexFmt, storeName, storeName)).Return(fmt.Errorf("error"))
|
||||
_, err := NewIndexer(indexers, store)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewIndexer() with TXClient Commit() error, should return error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
client := NewMockTXClient(gomock.NewController(t))
|
||||
|
||||
objKey := "objKey"
|
||||
indexers := map[string]cache.IndexFunc{
|
||||
"a": func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
}
|
||||
storeName := "someStoreName"
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(client, nil)
|
||||
store.EXPECT().GetName().AnyTimes().Return(storeName)
|
||||
client.EXPECT().Exec(fmt.Sprintf(createTableFmt, storeName, storeName)).Return(nil)
|
||||
client.EXPECT().Exec(fmt.Sprintf(createIndexFmt, storeName, storeName)).Return(nil)
|
||||
client.EXPECT().Commit().Return(fmt.Errorf("error"))
|
||||
_, err := NewIndexer(indexers, store)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestAfterUpsert(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
test func(t *testing.T)
|
||||
}
|
||||
|
||||
var tests []testCase
|
||||
|
||||
tests = append(tests, testCase{description: "AfterUpsert() with no errors returned from TXClient should return no error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
client := NewMockTXClient(gomock.NewController(t))
|
||||
deleteStmt := &sql.Stmt{}
|
||||
addStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
deleteIndicesStmt: deleteStmt,
|
||||
addIndexStmt: addStmt,
|
||||
indexers: map[string]cache.IndexFunc{
|
||||
"a": func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
key := "somekey"
|
||||
client.EXPECT().Stmt(indexer.deleteIndicesStmt).Return(indexer.deleteIndicesStmt)
|
||||
client.EXPECT().StmtExec(indexer.deleteIndicesStmt, key).Return(nil)
|
||||
client.EXPECT().Stmt(indexer.addIndexStmt).Return(indexer.addIndexStmt)
|
||||
client.EXPECT().StmtExec(indexer.addIndexStmt, "a", objKey, key).Return(nil)
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
err := indexer.AfterUpsert(key, testObject, client)
|
||||
assert.Nil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "AfterUpsert() with error returned from TXClient StmtExec() should return an error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
client := NewMockTXClient(gomock.NewController(t))
|
||||
deleteStmt := &sql.Stmt{}
|
||||
addStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
deleteIndicesStmt: deleteStmt,
|
||||
addIndexStmt: addStmt,
|
||||
indexers: map[string]cache.IndexFunc{
|
||||
"a": func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
key := "somekey"
|
||||
client.EXPECT().Stmt(indexer.deleteIndicesStmt).Return(indexer.deleteIndicesStmt)
|
||||
client.EXPECT().StmtExec(indexer.deleteIndicesStmt, key).Return(fmt.Errorf("error"))
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
err := indexer.AfterUpsert(key, testObject, client)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "AfterUpsert() with error returned from TXClient second StmtExec() call should return an error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
client := NewMockTXClient(gomock.NewController(t))
|
||||
deleteStmt := &sql.Stmt{}
|
||||
addStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
deleteIndicesStmt: deleteStmt,
|
||||
addIndexStmt: addStmt,
|
||||
indexers: map[string]cache.IndexFunc{
|
||||
"a": func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
key := "somekey"
|
||||
client.EXPECT().Stmt(indexer.deleteIndicesStmt).Return(indexer.deleteIndicesStmt)
|
||||
client.EXPECT().StmtExec(indexer.deleteIndicesStmt, key).Return(nil)
|
||||
client.EXPECT().Stmt(indexer.addIndexStmt).Return(indexer.addIndexStmt)
|
||||
client.EXPECT().StmtExec(indexer.addIndexStmt, "a", objKey, key).Return(fmt.Errorf("error"))
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
err := indexer.AfterUpsert(key, testObject, client)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
test func(t *testing.T)
|
||||
}
|
||||
|
||||
var tests []testCase
|
||||
|
||||
tests = append(tests, testCase{description: "Index() with no errors returned from store and 1 object returned by ReadObjects(), should return one obj and no error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
rows := &sql.Rows{}
|
||||
listStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
indexers: map[string]cache.IndexFunc{
|
||||
indexName: func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listByIndexStmt, indexName, objKey).Return(rows, nil)
|
||||
store.EXPECT().GetType().Return(reflect.TypeOf(testObject))
|
||||
store.EXPECT().GetShouldEncrypt().Return(false)
|
||||
store.EXPECT().ReadObjects(rows, reflect.TypeOf(testObject), false).Return([]any{testObject}, nil)
|
||||
objs, err := indexer.Index(indexName, testObject)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []any{testObject}, objs)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "Index() with no errors returned from store and multiple objects returned by ReadObjects(), should return multiple objects and no error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
rows := &sql.Rows{}
|
||||
listStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
indexers: map[string]cache.IndexFunc{
|
||||
indexName: func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listByIndexStmt, indexName, objKey).Return(rows, nil)
|
||||
store.EXPECT().GetType().Return(reflect.TypeOf(testObject))
|
||||
store.EXPECT().GetShouldEncrypt().Return(false)
|
||||
store.EXPECT().ReadObjects(rows, reflect.TypeOf(testObject), false).Return([]any{testObject, testObject}, nil)
|
||||
objs, err := indexer.Index(indexName, testObject)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []any{testObject, testObject}, objs)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "Index() with no errors returned from store and no objects returned by ReadObjects(), should return no objects and no error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
rows := &sql.Rows{}
|
||||
listStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
indexers: map[string]cache.IndexFunc{
|
||||
indexName: func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listByIndexStmt, indexName, objKey).Return(rows, nil)
|
||||
store.EXPECT().GetType().Return(reflect.TypeOf(testObject))
|
||||
store.EXPECT().GetShouldEncrypt().Return(false)
|
||||
store.EXPECT().ReadObjects(rows, reflect.TypeOf(testObject), false).Return([]any{}, nil)
|
||||
objs, err := indexer.Index(indexName, testObject)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []any{}, objs)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "Index() where index name is not in indexers, should return error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
listStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
indexers: map[string]cache.IndexFunc{
|
||||
indexName: func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
|
||||
_, err := indexer.Index("someotherindexname", testObject)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "Index() with an error returned from store QueryForRows, should return an error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
listStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
indexers: map[string]cache.IndexFunc{
|
||||
indexName: func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listByIndexStmt, indexName, objKey).Return(nil, fmt.Errorf("error"))
|
||||
_, err := indexer.Index(indexName, testObject)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "Index() with an errors returned from store ReadObjects(), should return an error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
rows := &sql.Rows{}
|
||||
listStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
indexers: map[string]cache.IndexFunc{
|
||||
indexName: func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listByIndexStmt, indexName, objKey).Return(rows, nil)
|
||||
store.EXPECT().GetType().Return(reflect.TypeOf(testObject))
|
||||
store.EXPECT().GetShouldEncrypt().Return(false)
|
||||
store.EXPECT().ReadObjects(rows, reflect.TypeOf(testObject), false).Return([]any{testObject}, fmt.Errorf("error"))
|
||||
_, err := indexer.Index(indexName, testObject)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "Index() with no errors returned from store and multiple keys returned from index func, should return one obj and no error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
rows := &sql.Rows{}
|
||||
listStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
indexers: map[string]cache.IndexFunc{
|
||||
indexName: func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey, objKey + "2"}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
|
||||
store.EXPECT().GetName().Return("name")
|
||||
stmt := &sql.Stmt{}
|
||||
store.EXPECT().Prepare(fmt.Sprintf(selectQueryFmt, "name", ", ?")).Return(stmt)
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listByIndexStmt, indexName, objKey, objKey+"2").Return(rows, nil)
|
||||
store.EXPECT().GetType().Return(reflect.TypeOf(testObject))
|
||||
store.EXPECT().GetShouldEncrypt().Return(false)
|
||||
store.EXPECT().ReadObjects(rows, reflect.TypeOf(testObject), false).Return([]any{testObject}, nil)
|
||||
store.EXPECT().CloseStmt(stmt).Return(nil)
|
||||
objs, err := indexer.Index(indexName, testObject)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []any{testObject}, objs)
|
||||
}})
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestByIndex(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
test func(t *testing.T)
|
||||
}
|
||||
|
||||
var tests []testCase
|
||||
|
||||
tests = append(tests, testCase{description: "IndexBy() with no errors returned from store and 1 object returned by ReadObjects(), should return one obj and no error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
rows := &sql.Rows{}
|
||||
listStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
}
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listByIndexStmt, indexName, objKey).Return(rows, nil)
|
||||
store.EXPECT().GetType().Return(reflect.TypeOf(testObject))
|
||||
store.EXPECT().GetShouldEncrypt().Return(false)
|
||||
store.EXPECT().ReadObjects(rows, reflect.TypeOf(testObject), false).Return([]any{testObject}, nil)
|
||||
objs, err := indexer.ByIndex(indexName, objKey)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []any{testObject}, objs)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "IndexBy() with no errors returned from store and multiple objects returned by ReadObjects(), should return multiple objects and no error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
rows := &sql.Rows{}
|
||||
listStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
}
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listByIndexStmt, indexName, objKey).Return(rows, nil)
|
||||
store.EXPECT().GetType().Return(reflect.TypeOf(testObject))
|
||||
store.EXPECT().GetShouldEncrypt().Return(false)
|
||||
store.EXPECT().ReadObjects(rows, reflect.TypeOf(testObject), false).Return([]any{testObject, testObject}, nil)
|
||||
objs, err := indexer.ByIndex(indexName, objKey)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []any{testObject, testObject}, objs)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "IndexBy() with no errors returned from store and no objects returned by ReadObjects(), should return no objects and no error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
rows := &sql.Rows{}
|
||||
listStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
}
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listByIndexStmt, indexName, objKey).Return(rows, nil)
|
||||
store.EXPECT().GetType().Return(reflect.TypeOf(testObject))
|
||||
store.EXPECT().GetShouldEncrypt().Return(false)
|
||||
store.EXPECT().ReadObjects(rows, reflect.TypeOf(testObject), false).Return([]any{}, nil)
|
||||
objs, err := indexer.ByIndex(indexName, objKey)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []any{}, objs)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "IndexBy() with an error returned from store QueryForRows, should return an error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
listStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
}
|
||||
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listByIndexStmt, indexName, objKey).Return(nil, fmt.Errorf("error"))
|
||||
_, err := indexer.ByIndex(indexName, objKey)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "IndexBy() with an errors returned from store ReadObjects(), should return an error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
rows := &sql.Rows{}
|
||||
listStmt := &sql.Stmt{}
|
||||
objKey := "key"
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
}
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listByIndexStmt, indexName, objKey).Return(rows, nil)
|
||||
store.EXPECT().GetType().Return(reflect.TypeOf(testObject))
|
||||
store.EXPECT().GetShouldEncrypt().Return(false)
|
||||
store.EXPECT().ReadObjects(rows, reflect.TypeOf(testObject), false).Return([]any{testObject}, fmt.Errorf("error"))
|
||||
_, err := indexer.ByIndex(indexName, objKey)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestListIndexFuncValues(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
test func(t *testing.T)
|
||||
}
|
||||
|
||||
var tests []testCase
|
||||
|
||||
tests = append(tests, testCase{description: "ListIndexFuncvalues() with no errors returned from store and 1 object returned by ReadObjects(), should return one obj and no error", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
rows := &sql.Rows{}
|
||||
listStmt := &sql.Stmt{}
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
}
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listIndexValuesStmt, indexName).Return(rows, nil)
|
||||
store.EXPECT().ReadStrings(rows).Return([]string{"somestrings"}, nil)
|
||||
vals := indexer.ListIndexFuncValues(indexName)
|
||||
assert.Equal(t, []string{"somestrings"}, vals)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "ListIndexFuncvalues() with QueryForRows() error returned from store, should panic", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
listStmt := &sql.Stmt{}
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
}
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listIndexValuesStmt, indexName).Return(nil, fmt.Errorf("error"))
|
||||
assert.Panics(t, func() { indexer.ListIndexFuncValues(indexName) })
|
||||
}})
|
||||
tests = append(tests, testCase{description: "ListIndexFuncvalues() with ReadStrings() error returned from store, should panic", test: func(t *testing.T) {
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
rows := &sql.Rows{}
|
||||
listStmt := &sql.Stmt{}
|
||||
indexName := "someindexname"
|
||||
indexer := &Indexer{
|
||||
Store: store,
|
||||
listByIndexStmt: listStmt,
|
||||
}
|
||||
store.EXPECT().QueryForRows(context.TODO(), indexer.listIndexValuesStmt, indexName).Return(rows, nil)
|
||||
store.EXPECT().ReadStrings(rows).Return([]string{"somestrings"}, fmt.Errorf("error"))
|
||||
assert.Panics(t, func() { indexer.ListIndexFuncValues(indexName) })
|
||||
}})
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIndexers(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
test func(t *testing.T)
|
||||
}
|
||||
|
||||
var tests []testCase
|
||||
|
||||
tests = append(tests, testCase{description: "GetIndexers() should return indexers fron indexers field", test: func(t *testing.T) {
|
||||
objKey := "key"
|
||||
expectedIndexers := map[string]cache.IndexFunc{
|
||||
"a": func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
}
|
||||
indexer := &Indexer{
|
||||
indexers: expectedIndexers,
|
||||
}
|
||||
indexers := indexer.GetIndexers()
|
||||
assert.Equal(t, cache.Indexers(expectedIndexers), indexers)
|
||||
}})
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddIndexers(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
test func(t *testing.T)
|
||||
}
|
||||
|
||||
var tests []testCase
|
||||
|
||||
tests = append(tests, testCase{description: "GetIndexers() should return indexers fron indexers field", test: func(t *testing.T) {
|
||||
objKey := "key"
|
||||
expectedIndexers := map[string]cache.IndexFunc{
|
||||
"a": func(obj interface{}) ([]string, error) {
|
||||
return []string{objKey}, nil
|
||||
},
|
||||
}
|
||||
indexer := &Indexer{}
|
||||
err := indexer.AddIndexers(expectedIndexers)
|
||||
assert.Nil(t, err)
|
||||
assert.ObjectsAreEqual(cache.Indexers(expectedIndexers), indexer.indexers)
|
||||
}})
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
||||
}
|
||||
}
|
94
pkg/sqlcache/informer/informer.go
Normal file
94
pkg/sqlcache/informer/informer.go
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
package sql provides an Informer and Indexer that uses SQLite as a store, instead of an in-memory store like a map.
|
||||
*/
|
||||
|
||||
package informer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/steve/pkg/sqlcache/partition"
|
||||
sqlStore "github.com/rancher/steve/pkg/sqlcache/store"
|
||||
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/dynamic"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
// Informer is a SQLite-backed cache.SharedIndexInformer that can execute queries on listprocessor structs
|
||||
type Informer struct {
|
||||
cache.SharedIndexInformer
|
||||
ByOptionsLister
|
||||
}
|
||||
|
||||
type ByOptionsLister interface {
|
||||
ListByOptions(ctx context.Context, lo ListOptions, partitions []partition.Partition, namespace string) (*unstructured.UnstructuredList, int, string, error)
|
||||
}
|
||||
|
||||
// this is set to a var so that it can be overridden by test code for mocking purposes
|
||||
var newInformer = cache.NewSharedIndexInformer
|
||||
|
||||
// NewInformer returns a new SQLite-backed Informer for the type specified by schema in unstructured.Unstructured form
|
||||
// using the specified client
|
||||
func NewInformer(client dynamic.ResourceInterface, fields [][]string, transform cache.TransformFunc, gvk schema.GroupVersionKind, db sqlStore.DBClient, shouldEncrypt bool, namespaced bool) (*Informer, error) {
|
||||
listWatcher := &cache.ListWatch{
|
||||
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
|
||||
a, err := client.List(context.Background(), options)
|
||||
return a, err
|
||||
},
|
||||
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
|
||||
return client.Watch(context.Background(), options)
|
||||
},
|
||||
}
|
||||
|
||||
example := &unstructured.Unstructured{}
|
||||
example.SetGroupVersionKind(gvk)
|
||||
|
||||
// avoids the informer to periodically resync (re-list) its resources
|
||||
// currently it is a work hypothesis that, when interacting with the UI, this should not be needed
|
||||
resyncPeriod := time.Duration(0)
|
||||
|
||||
sii := newInformer(listWatcher, example, resyncPeriod, cache.Indexers{})
|
||||
if transform != nil {
|
||||
if err := sii.SetTransform(transform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
name := informerNameFromGVK(gvk)
|
||||
|
||||
s, err := sqlStore.NewStore(example, cache.DeletionHandlingMetaNamespaceKeyFunc, db, shouldEncrypt, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loi, err := NewListOptionIndexer(fields, s, namespaced)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// HACK: replace the default informer's indexer with the SQL based one
|
||||
UnsafeSet(sii, "indexer", loi)
|
||||
|
||||
return &Informer{
|
||||
SharedIndexInformer: sii,
|
||||
ByOptionsLister: loi,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListByOptions returns objects according to the specified list options and partitions.
|
||||
// Specifically:
|
||||
// - an unstructured list of resources belonging to any of the specified partitions
|
||||
// - the total number of resources (returned list might be a subset depending on pagination options in lo)
|
||||
// - a continue token, if there are more pages after the returned one
|
||||
// - an error instead of all of the above if anything went wrong
|
||||
func (i *Informer) ListByOptions(ctx context.Context, lo ListOptions, partitions []partition.Partition, namespace string) (*unstructured.UnstructuredList, int, string, error) {
|
||||
return i.ByOptionsLister.ListByOptions(ctx, lo, partitions, namespace)
|
||||
}
|
||||
|
||||
func informerNameFromGVK(gvk schema.GroupVersionKind) string {
|
||||
return gvk.Group + "_" + gvk.Version + "_" + gvk.Kind
|
||||
}
|
59
pkg/sqlcache/informer/informer_mocks_test.go
Normal file
59
pkg/sqlcache/informer/informer_mocks_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/rancher/steve/pkg/sqlcache/informer (interfaces: ByOptionsLister)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen --build_flags=--mod=mod -package informer -destination ./informer_mocks_test.go github.com/rancher/steve/pkg/sqlcache/informer ByOptionsLister
|
||||
//
|
||||
|
||||
// Package informer is a generated GoMock package.
|
||||
package informer
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
partition "github.com/rancher/steve/pkg/sqlcache/partition"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// MockByOptionsLister is a mock of ByOptionsLister interface.
|
||||
type MockByOptionsLister struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockByOptionsListerMockRecorder
|
||||
}
|
||||
|
||||
// MockByOptionsListerMockRecorder is the mock recorder for MockByOptionsLister.
|
||||
type MockByOptionsListerMockRecorder struct {
|
||||
mock *MockByOptionsLister
|
||||
}
|
||||
|
||||
// NewMockByOptionsLister creates a new mock instance.
|
||||
func NewMockByOptionsLister(ctrl *gomock.Controller) *MockByOptionsLister {
|
||||
mock := &MockByOptionsLister{ctrl: ctrl}
|
||||
mock.recorder = &MockByOptionsListerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockByOptionsLister) EXPECT() *MockByOptionsListerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ListByOptions mocks base method.
|
||||
func (m *MockByOptionsLister) ListByOptions(arg0 context.Context, arg1 ListOptions, arg2 []partition.Partition, arg3 string) (*unstructured.UnstructuredList, int, string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListByOptions", arg0, arg1, arg2, arg3)
|
||||
ret0, _ := ret[0].(*unstructured.UnstructuredList)
|
||||
ret1, _ := ret[1].(int)
|
||||
ret2, _ := ret[2].(string)
|
||||
ret3, _ := ret[3].(error)
|
||||
return ret0, ret1, ret2, ret3
|
||||
}
|
||||
|
||||
// ListByOptions indicates an expected call of ListByOptions.
|
||||
func (mr *MockByOptionsListerMockRecorder) ListByOptions(arg0, arg1, arg2, arg3 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByOptions", reflect.TypeOf((*MockByOptionsLister)(nil).ListByOptions), arg0, arg1, arg2, arg3)
|
||||
}
|
345
pkg/sqlcache/informer/informer_test.go
Normal file
345
pkg/sqlcache/informer/informer_test.go
Normal file
@@ -0,0 +1,345 @@
|
||||
package informer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/steve/pkg/sqlcache/partition"
|
||||
"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
|
||||
//go:generate mockgen --build_flags=--mod=mod -package informer -destination ./store_mocks_test.go github.com/rancher/steve/pkg/sqlcache/store DBClient
|
||||
|
||||
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 := NewMockDBClient(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.
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
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
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
|
||||
// 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
|
||||
dbClient.EXPECT().BeginTx(context.Background(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
|
||||
informer, err := NewInformer(dynamicClient, fields, nil, gvk, dbClient, false, 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 := NewMockDBClient(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.
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(fmt.Errorf("error"))
|
||||
|
||||
_, err := NewInformer(dynamicClient, fields, nil, gvk, dbClient, false, 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 := NewMockDBClient(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.
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
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
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(fmt.Errorf("error"))
|
||||
|
||||
_, err := NewInformer(dynamicClient, fields, nil, gvk, dbClient, false, 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 := NewMockDBClient(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.
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
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
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
|
||||
// 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
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(fmt.Errorf("error"))
|
||||
|
||||
_, err := NewInformer(dynamicClient, fields, nil, gvk, dbClient, false, true)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewInformer() with transform func", test: func(t *testing.T) {
|
||||
dbClient := NewMockDBClient(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.
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
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
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
|
||||
// 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
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
|
||||
transformFunc := func(input interface{}) (interface{}, error) {
|
||||
return "someoutput", nil
|
||||
}
|
||||
informer, err := NewInformer(dynamicClient, fields, transformFunc, gvk, dbClient, false, 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 := NewMockDBClient(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(dynamicClient, fields, transformFunc, gvk, dbClient, false, 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 := 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.TODO(), lo, partitions, ns).Return(expectedList, expectedTotal, expectedContinueToken, nil)
|
||||
list, total, continueToken, err := informer.ListByOptions(context.TODO(), 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 := ListOptions{}
|
||||
var partitions []partition.Partition
|
||||
ns := "somens"
|
||||
indexer.EXPECT().ListByOptions(context.TODO(), lo, partitions, ns).Return(nil, 0, "", fmt.Errorf("error"))
|
||||
_, _, _, err := informer.ListByOptions(context.TODO(), 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
|
||||
}
|
551
pkg/sqlcache/informer/listoption_indexer.go
Normal file
551
pkg/sqlcache/informer/listoption_indexer.go
Normal file
@@ -0,0 +1,551 @@
|
||||
package informer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/rancher/steve/pkg/sqlcache/db"
|
||||
"github.com/rancher/steve/pkg/sqlcache/partition"
|
||||
)
|
||||
|
||||
// ListOptionIndexer extends Indexer by allowing queries based on ListOption
|
||||
type ListOptionIndexer struct {
|
||||
*Indexer
|
||||
|
||||
namespaced bool
|
||||
indexedFields []string
|
||||
|
||||
addFieldQuery string
|
||||
deleteFieldQuery string
|
||||
|
||||
addFieldStmt *sql.Stmt
|
||||
deleteFieldStmt *sql.Stmt
|
||||
}
|
||||
|
||||
var (
|
||||
defaultIndexedFields = []string{"metadata.name", "metadata.creationTimestamp"}
|
||||
defaultIndexNamespaced = "metadata.namespace"
|
||||
subfieldRegex = regexp.MustCompile(`([a-zA-Z]+)|(\[[a-zA-Z./]+])|(\[[0-9]+])`)
|
||||
|
||||
ErrInvalidColumn = errors.New("supplied column is invalid")
|
||||
)
|
||||
|
||||
const (
|
||||
matchFmt = `%%%s%%`
|
||||
strictMatchFmt = `%s`
|
||||
createFieldsTableFmt = `CREATE TABLE "%s_fields" (
|
||||
key TEXT NOT NULL PRIMARY KEY,
|
||||
%s
|
||||
)`
|
||||
createFieldsIndexFmt = `CREATE INDEX "%s_%s_index" ON "%s_fields"("%s")`
|
||||
|
||||
failedToGetFromSliceFmt = "[listoption indexer] failed to get subfield [%s] from slice items: %w"
|
||||
)
|
||||
|
||||
// NewListOptionIndexer returns a SQLite-backed cache.Indexer of unstructured.Unstructured Kubernetes resources of a certain GVK
|
||||
// ListOptionIndexer is also able to satisfy ListOption queries on indexed (sub)fields
|
||||
// Fields are specified as slices (eg. "metadata.resourceVersion" is ["metadata", "resourceVersion"])
|
||||
func NewListOptionIndexer(fields [][]string, s Store, namespaced bool) (*ListOptionIndexer, error) {
|
||||
// necessary in order to gob/ungob unstructured.Unstructured objects
|
||||
gob.Register(map[string]interface{}{})
|
||||
gob.Register([]interface{}{})
|
||||
|
||||
i, err := NewIndexer(cache.Indexers{}, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var indexedFields []string
|
||||
for _, f := range defaultIndexedFields {
|
||||
indexedFields = append(indexedFields, f)
|
||||
}
|
||||
if namespaced {
|
||||
indexedFields = append(indexedFields, defaultIndexNamespaced)
|
||||
}
|
||||
for _, f := range fields {
|
||||
indexedFields = append(indexedFields, toColumnName(f))
|
||||
}
|
||||
|
||||
l := &ListOptionIndexer{
|
||||
Indexer: i,
|
||||
namespaced: namespaced,
|
||||
indexedFields: indexedFields,
|
||||
}
|
||||
l.RegisterAfterUpsert(l.afterUpsert)
|
||||
l.RegisterAfterDelete(l.afterDelete)
|
||||
columnDefs := make([]string, len(indexedFields))
|
||||
for index, field := range indexedFields {
|
||||
column := fmt.Sprintf(`"%s" TEXT`, field)
|
||||
columnDefs[index] = column
|
||||
}
|
||||
|
||||
tx, err := l.BeginTx(context.Background(), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = tx.Exec(fmt.Sprintf(createFieldsTableFmt, db.Sanitize(i.GetName()), strings.Join(columnDefs, ", ")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
columns := make([]string, len(indexedFields))
|
||||
qmarks := make([]string, len(indexedFields))
|
||||
setStatements := make([]string, len(indexedFields))
|
||||
|
||||
for index, field := range indexedFields {
|
||||
// create index for field
|
||||
err = tx.Exec(fmt.Sprintf(createFieldsIndexFmt, db.Sanitize(i.GetName()), field, db.Sanitize(i.GetName()), field))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// format field into column for prepared statement
|
||||
column := fmt.Sprintf(`"%s"`, field)
|
||||
columns[index] = column
|
||||
|
||||
// add placeholder for column's value in prepared statement
|
||||
qmarks[index] = "?"
|
||||
|
||||
// add formatted set statement for prepared statement
|
||||
setStatement := fmt.Sprintf(`"%s" = excluded."%s"`, field, field)
|
||||
setStatements[index] = setStatement
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.addFieldQuery = fmt.Sprintf(
|
||||
`INSERT INTO "%s_fields"(key, %s) VALUES (?, %s) ON CONFLICT DO UPDATE SET %s`,
|
||||
db.Sanitize(i.GetName()),
|
||||
strings.Join(columns, ", "),
|
||||
strings.Join(qmarks, ", "),
|
||||
strings.Join(setStatements, ", "),
|
||||
)
|
||||
l.deleteFieldQuery = fmt.Sprintf(`DELETE FROM "%s_fields" WHERE key = ?`, db.Sanitize(i.GetName()))
|
||||
|
||||
l.addFieldStmt = l.Prepare(l.addFieldQuery)
|
||||
l.deleteFieldStmt = l.Prepare(l.deleteFieldQuery)
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
/* Core methods */
|
||||
|
||||
// afterUpsert saves sortable/filterable fields into tables
|
||||
func (l *ListOptionIndexer) afterUpsert(key string, obj any, tx db.TXClient) error {
|
||||
args := []any{key}
|
||||
for _, field := range l.indexedFields {
|
||||
value, err := getField(obj, field)
|
||||
if err != nil {
|
||||
logrus.Errorf("cannot index object of type [%s] with key [%s] for indexer [%s]: %v", l.GetType().String(), key, l.GetName(), err)
|
||||
cErr := tx.Cancel()
|
||||
if cErr != nil {
|
||||
return fmt.Errorf("could not cancel transaction: %s while recovering from error: %w", cErr, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
switch typedValue := value.(type) {
|
||||
case nil:
|
||||
args = append(args, "")
|
||||
case int, bool, string:
|
||||
args = append(args, fmt.Sprint(typedValue))
|
||||
case []string:
|
||||
args = append(args, strings.Join(typedValue, "|"))
|
||||
default:
|
||||
err2 := fmt.Errorf("field %v has a non-supported type value: %v", field, value)
|
||||
cErr := tx.Cancel()
|
||||
if cErr != nil {
|
||||
return fmt.Errorf("could not cancel transaction: %s while recovering from error: %w", cErr, err2)
|
||||
}
|
||||
return err2
|
||||
}
|
||||
}
|
||||
|
||||
err := tx.StmtExec(tx.Stmt(l.addFieldStmt), args...)
|
||||
if err != nil {
|
||||
return &db.QueryError{QueryString: l.addFieldQuery, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *ListOptionIndexer) afterDelete(key string, tx db.TXClient) error {
|
||||
args := []any{key}
|
||||
|
||||
err := tx.StmtExec(tx.Stmt(l.deleteFieldStmt), args...)
|
||||
if err != nil {
|
||||
return &db.QueryError{QueryString: l.deleteFieldQuery, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListByOptions returns objects according to the specified list options and partitions.
|
||||
// Specifically:
|
||||
// - an unstructured list of resources belonging to any of the specified partitions
|
||||
// - the total number of resources (returned list might be a subset depending on pagination options in lo)
|
||||
// - a continue token, if there are more pages after the returned one
|
||||
// - an error instead of all of the above if anything went wrong
|
||||
func (l *ListOptionIndexer) ListByOptions(ctx context.Context, lo ListOptions, partitions []partition.Partition, namespace string) (*unstructured.UnstructuredList, int, string, error) {
|
||||
// 1- Intro: SELECT and JOIN clauses
|
||||
query := fmt.Sprintf(`SELECT o.object, o.objectnonce, o.dekid FROM "%s" o`, db.Sanitize(l.GetName()))
|
||||
query += "\n "
|
||||
query += fmt.Sprintf(`JOIN "%s_fields" f ON o.key = f.key`, db.Sanitize(l.GetName()))
|
||||
params := []any{}
|
||||
|
||||
// 2- Filtering: WHERE clauses (from lo.Filters)
|
||||
whereClauses := []string{}
|
||||
for _, orFilters := range lo.Filters {
|
||||
orClause, orParams, err := l.buildORClauseFromFilters(orFilters)
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
if orClause == "" {
|
||||
continue
|
||||
}
|
||||
whereClauses = append(whereClauses, orClause)
|
||||
params = append(params, orParams...)
|
||||
}
|
||||
|
||||
// WHERE clauses (from namespace)
|
||||
if namespace != "" && namespace != "*" {
|
||||
whereClauses = append(whereClauses, fmt.Sprintf(`f."metadata.namespace" = ?`))
|
||||
params = append(params, namespace)
|
||||
}
|
||||
|
||||
// WHERE clauses (from partitions and their corresponding parameters)
|
||||
partitionClauses := []string{}
|
||||
for _, partition := range partitions {
|
||||
if partition.Passthrough {
|
||||
// nothing to do, no extra filtering to apply by definition
|
||||
} else {
|
||||
singlePartitionClauses := []string{}
|
||||
|
||||
// filter by namespace
|
||||
if partition.Namespace != "" && partition.Namespace != "*" {
|
||||
singlePartitionClauses = append(singlePartitionClauses, fmt.Sprintf(`f."metadata.namespace" = ?`))
|
||||
params = append(params, partition.Namespace)
|
||||
}
|
||||
|
||||
// optionally filter by names
|
||||
if !partition.All {
|
||||
names := partition.Names
|
||||
|
||||
if len(names) == 0 {
|
||||
// degenerate case, there will be no results
|
||||
singlePartitionClauses = append(singlePartitionClauses, "FALSE")
|
||||
} else {
|
||||
singlePartitionClauses = append(singlePartitionClauses, fmt.Sprintf(`f."metadata.name" IN (?%s)`, strings.Repeat(", ?", len(partition.Names)-1)))
|
||||
// sort for reproducibility
|
||||
sortedNames := partition.Names.UnsortedList()
|
||||
sort.Strings(sortedNames)
|
||||
for _, name := range sortedNames {
|
||||
params = append(params, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(singlePartitionClauses) > 0 {
|
||||
partitionClauses = append(partitionClauses, strings.Join(singlePartitionClauses, " AND "))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(partitions) == 0 {
|
||||
// degenerate case, there will be no results
|
||||
whereClauses = append(whereClauses, "FALSE")
|
||||
}
|
||||
if len(partitionClauses) == 1 {
|
||||
whereClauses = append(whereClauses, partitionClauses[0])
|
||||
}
|
||||
if len(partitionClauses) > 1 {
|
||||
whereClauses = append(whereClauses, "(\n ("+strings.Join(partitionClauses, ") OR\n (")+")\n)")
|
||||
}
|
||||
|
||||
if len(whereClauses) > 0 {
|
||||
query += "\n WHERE\n "
|
||||
for index, clause := range whereClauses {
|
||||
query += fmt.Sprintf("(%s)", clause)
|
||||
if index == len(whereClauses)-1 {
|
||||
break
|
||||
}
|
||||
query += " AND\n "
|
||||
}
|
||||
}
|
||||
|
||||
// 2- Sorting: ORDER BY clauses (from lo.Sort)
|
||||
orderByClauses := []string{}
|
||||
if len(lo.Sort.PrimaryField) > 0 {
|
||||
columnName := toColumnName(lo.Sort.PrimaryField)
|
||||
if err := l.validateColumn(columnName); err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
|
||||
direction := "ASC"
|
||||
if lo.Sort.PrimaryOrder == DESC {
|
||||
direction = "DESC"
|
||||
}
|
||||
orderByClauses = append(orderByClauses, fmt.Sprintf(`f."%s" %s`, columnName, direction))
|
||||
}
|
||||
if len(lo.Sort.SecondaryField) > 0 {
|
||||
columnName := toColumnName(lo.Sort.SecondaryField)
|
||||
if err := l.validateColumn(columnName); err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
|
||||
direction := "ASC"
|
||||
if lo.Sort.SecondaryOrder == DESC {
|
||||
direction = "DESC"
|
||||
}
|
||||
orderByClauses = append(orderByClauses, fmt.Sprintf(`f."%s" %s`, columnName, direction))
|
||||
}
|
||||
|
||||
if len(orderByClauses) > 0 {
|
||||
query += "\n ORDER BY "
|
||||
query += strings.Join(orderByClauses, ", ")
|
||||
} else {
|
||||
// make sure one default order is always picked
|
||||
if l.namespaced {
|
||||
query += "\n ORDER BY f.\"metadata.namespace\" ASC, f.\"metadata.name\" ASC "
|
||||
} else {
|
||||
query += "\n ORDER BY f.\"metadata.name\" ASC "
|
||||
}
|
||||
}
|
||||
|
||||
// 4- Pagination: LIMIT clause (from lo.Pagination and/or lo.ChunkSize/lo.Resume)
|
||||
|
||||
// before proceeding, save a copy of the query and params without LIMIT/OFFSET
|
||||
// for COUNTing all results later
|
||||
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM (%s)", query)
|
||||
countParams := params[:]
|
||||
|
||||
limitClause := ""
|
||||
// take the smallest limit between lo.Pagination and lo.ChunkSize
|
||||
limit := lo.Pagination.PageSize
|
||||
if limit == 0 || (lo.ChunkSize > 0 && lo.ChunkSize < limit) {
|
||||
limit = lo.ChunkSize
|
||||
}
|
||||
if limit > 0 {
|
||||
limitClause = "\n LIMIT ?"
|
||||
params = append(params, limit)
|
||||
}
|
||||
|
||||
// OFFSET clause (from lo.Pagination and/or lo.Resume)
|
||||
offsetClause := ""
|
||||
offset := 0
|
||||
if lo.Resume != "" {
|
||||
offsetInt, err := strconv.Atoi(lo.Resume)
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
offset = offsetInt
|
||||
}
|
||||
if lo.Pagination.Page >= 1 {
|
||||
offset += lo.Pagination.PageSize * (lo.Pagination.Page - 1)
|
||||
}
|
||||
if offset > 0 {
|
||||
offsetClause = "\n OFFSET ?"
|
||||
params = append(params, offset)
|
||||
}
|
||||
|
||||
// assemble and log the final query
|
||||
query += limitClause
|
||||
query += offsetClause
|
||||
logrus.Debugf("ListOptionIndexer prepared statement: %v", query)
|
||||
logrus.Debugf("Params: %v", params)
|
||||
|
||||
// execute
|
||||
stmt := l.Prepare(query)
|
||||
defer l.CloseStmt(stmt)
|
||||
|
||||
tx, err := l.BeginTx(ctx, false)
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
|
||||
txStmt := tx.Stmt(stmt)
|
||||
rows, err := txStmt.QueryContext(ctx, params...)
|
||||
if err != nil {
|
||||
if cerr := tx.Cancel(); cerr != nil {
|
||||
return nil, 0, "", fmt.Errorf("failed to cancel transaction (%v) after error: %w", cerr, err)
|
||||
}
|
||||
return nil, 0, "", &db.QueryError{QueryString: query, Err: err}
|
||||
}
|
||||
items, err := l.ReadObjects(rows, l.GetType(), l.GetShouldEncrypt())
|
||||
if err != nil {
|
||||
if cerr := tx.Cancel(); cerr != nil {
|
||||
return nil, 0, "", fmt.Errorf("failed to cancel transaction (%v) after error: %w", cerr, err)
|
||||
}
|
||||
return nil, 0, "", err
|
||||
}
|
||||
|
||||
total := len(items)
|
||||
// if limit or offset were set, execute counting of all rows
|
||||
if limit > 0 || offset > 0 {
|
||||
countStmt := l.Prepare(countQuery)
|
||||
defer l.CloseStmt(countStmt)
|
||||
txStmt := tx.Stmt(countStmt)
|
||||
rows, err := txStmt.QueryContext(ctx, countParams...)
|
||||
if err != nil {
|
||||
if cerr := tx.Cancel(); cerr != nil {
|
||||
return nil, 0, "", fmt.Errorf("failed to cancel transaction (%v) after error: %w", cerr, err)
|
||||
}
|
||||
return nil, 0, "", fmt.Errorf("error executing query: %w", err)
|
||||
}
|
||||
total, err = l.ReadInt(rows)
|
||||
if err != nil {
|
||||
if cerr := tx.Cancel(); cerr != nil {
|
||||
return nil, 0, "", fmt.Errorf("failed to cancel transaction (%v) after error: %w", cerr, err)
|
||||
}
|
||||
return nil, 0, "", fmt.Errorf("error reading query results: %w", err)
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
|
||||
continueToken := ""
|
||||
if limit > 0 && offset+len(items) < total {
|
||||
continueToken = fmt.Sprintf("%d", offset+limit)
|
||||
}
|
||||
|
||||
return toUnstructuredList(items), total, continueToken, nil
|
||||
}
|
||||
|
||||
func (l *ListOptionIndexer) validateColumn(column string) error {
|
||||
for _, v := range l.indexedFields {
|
||||
if v == column {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("column is invalid [%s]: %w", column, ErrInvalidColumn)
|
||||
}
|
||||
|
||||
// buildORClause creates an SQLite compatible query that ORs conditions built from passed filters
|
||||
func (l *ListOptionIndexer) buildORClauseFromFilters(orFilters OrFilter) (string, []any, error) {
|
||||
var orWhereClause string
|
||||
var params []any
|
||||
|
||||
for index, filter := range orFilters.Filters {
|
||||
opString := "LIKE"
|
||||
if filter.Op == NotEq {
|
||||
opString = "NOT LIKE"
|
||||
}
|
||||
columnName := toColumnName(filter.Field)
|
||||
if err := l.validateColumn(columnName); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
orWhereClause += fmt.Sprintf(`f."%s" %s ? ESCAPE '\'`, columnName, opString)
|
||||
format := strictMatchFmt
|
||||
if filter.Partial {
|
||||
format = matchFmt
|
||||
}
|
||||
match := filter.Match
|
||||
// To allow matches on the backslash itself, the character needs to be replaced first.
|
||||
// Otherwise, it will undo the following replacements.
|
||||
match = strings.ReplaceAll(match, `\`, `\\`)
|
||||
match = strings.ReplaceAll(match, `_`, `\_`)
|
||||
match = strings.ReplaceAll(match, `%`, `\%`)
|
||||
params = append(params, fmt.Sprintf(format, match))
|
||||
if index == len(orFilters.Filters)-1 {
|
||||
continue
|
||||
}
|
||||
orWhereClause += " OR "
|
||||
}
|
||||
return orWhereClause, params, nil
|
||||
}
|
||||
|
||||
// toColumnName returns the column name corresponding to a field expressed as string slice
|
||||
func toColumnName(s []string) string {
|
||||
return db.Sanitize(strings.Join(s, "."))
|
||||
}
|
||||
|
||||
// getField extracts the value of a field expressed as a string path from an unstructured object
|
||||
func getField(a any, field string) (any, error) {
|
||||
subFields := extractSubFields(field)
|
||||
o, ok := a.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected object type, expected unstructured.Unstructured: %v", a)
|
||||
}
|
||||
|
||||
var obj interface{}
|
||||
var found bool
|
||||
var err error
|
||||
obj = o.Object
|
||||
for i, subField := range subFields {
|
||||
switch t := obj.(type) {
|
||||
case map[string]interface{}:
|
||||
subField = strings.TrimSuffix(strings.TrimPrefix(subField, "["), "]")
|
||||
obj, found, err = unstructured.NestedFieldNoCopy(t, subField)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
// particularly with labels/annotation indexes, it is totally possible that some objects won't have these,
|
||||
// so either we this is not an error state or it could be an error state with a type that callers can check for
|
||||
return nil, nil
|
||||
}
|
||||
case []interface{}:
|
||||
if strings.HasPrefix(subField, "[") && strings.HasSuffix(subField, "]") {
|
||||
key, err := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(subField, "["), "]"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[listoption indexer] failed to convert subfield [%s] to int in listoption index: %w", subField, err)
|
||||
}
|
||||
if key >= len(t) {
|
||||
return nil, fmt.Errorf("[listoption indexer] given index is too large for slice of len %d", len(t))
|
||||
}
|
||||
obj = fmt.Sprintf("%v", t[key])
|
||||
} else if i == len(subFields)-1 {
|
||||
result := make([]string, len(t))
|
||||
for index, v := range t {
|
||||
itemVal, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(failedToGetFromSliceFmt, subField, err)
|
||||
}
|
||||
itemStr, ok := itemVal[subField].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(failedToGetFromSliceFmt, subField, err)
|
||||
}
|
||||
result[index] = itemStr
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("[listoption indexer] failed to parse subfields: %v", subFields)
|
||||
}
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func extractSubFields(fields string) []string {
|
||||
subfields := make([]string, 0)
|
||||
for _, subField := range subfieldRegex.FindAllString(fields, -1) {
|
||||
subfields = append(subfields, strings.TrimSuffix(subField, "."))
|
||||
}
|
||||
return subfields
|
||||
}
|
||||
|
||||
// toUnstructuredList turns a slice of unstructured objects into an unstructured.UnstructuredList
|
||||
func toUnstructuredList(items []any) *unstructured.UnstructuredList {
|
||||
objectItems := make([]map[string]any, len(items))
|
||||
result := &unstructured.UnstructuredList{
|
||||
Items: make([]unstructured.Unstructured, len(items)),
|
||||
Object: map[string]interface{}{"items": objectItems},
|
||||
}
|
||||
for i, item := range items {
|
||||
result.Items[i] = *item.(*unstructured.Unstructured)
|
||||
objectItems[i] = item.(*unstructured.Unstructured).Object
|
||||
}
|
||||
return result
|
||||
}
|
751
pkg/sqlcache/informer/listoption_indexer_test.go
Normal file
751
pkg/sqlcache/informer/listoption_indexer_test.go
Normal file
@@ -0,0 +1,751 @@
|
||||
/*
|
||||
Copyright 2023 SUSE LLC
|
||||
|
||||
Adapted from client-go, Copyright 2014 The Kubernetes Authors.
|
||||
*/
|
||||
|
||||
package informer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
"github.com/rancher/steve/pkg/sqlcache/partition"
|
||||
)
|
||||
|
||||
func TestNewListOptionIndexer(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
test func(t *testing.T)
|
||||
}
|
||||
|
||||
var tests []testCase
|
||||
tests = append(tests, testCase{description: "NewListOptionIndexer() with no errors returned, should return no error", test: func(t *testing.T) {
|
||||
txClient := NewMockTXClient(gomock.NewController(t))
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
fields := [][]string{{"something"}}
|
||||
id := "somename"
|
||||
stmt := &sql.Stmt{}
|
||||
// logic for NewIndexer(), only interested in if this results in error or not
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
store.EXPECT().GetName().Return(id).AnyTimes()
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
store.EXPECT().RegisterAfterUpsert(gomock.Any())
|
||||
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
|
||||
// end NewIndexer() logic
|
||||
|
||||
store.EXPECT().RegisterAfterUpsert(gomock.Any())
|
||||
store.EXPECT().RegisterAfterDelete(gomock.Any())
|
||||
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
// create field table
|
||||
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsTableFmt, id, `"metadata.name" TEXT, "metadata.creationTimestamp" TEXT, "metadata.namespace" TEXT, "something" TEXT`)).Return(nil)
|
||||
// create field table indexes
|
||||
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.name", id, "metadata.name")).Return(nil)
|
||||
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.namespace", id, "metadata.namespace")).Return(nil)
|
||||
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.creationTimestamp", id, "metadata.creationTimestamp")).Return(nil)
|
||||
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, fields[0][0], id, fields[0][0])).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
|
||||
loi, err := NewListOptionIndexer(fields, store, true)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, loi)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewListOptionIndexer() with error returned from NewIndxer(), should return an error", test: func(t *testing.T) {
|
||||
txClient := NewMockTXClient(gomock.NewController(t))
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
fields := [][]string{{"something"}}
|
||||
id := "somename"
|
||||
// logic for NewIndexer(), only interested in if this results in error or not
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
store.EXPECT().GetName().Return(id).AnyTimes()
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(fmt.Errorf("error"))
|
||||
|
||||
_, err := NewListOptionIndexer(fields, store, false)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewListOptionIndexer() with error returned from Begin(), should return an error", test: func(t *testing.T) {
|
||||
txClient := NewMockTXClient(gomock.NewController(t))
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
fields := [][]string{{"something"}}
|
||||
id := "somename"
|
||||
stmt := &sql.Stmt{}
|
||||
// logic for NewIndexer(), only interested in if this results in error or not
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
store.EXPECT().GetName().Return(id).AnyTimes()
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
store.EXPECT().RegisterAfterUpsert(gomock.Any())
|
||||
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
|
||||
// end NewIndexer() logic
|
||||
|
||||
store.EXPECT().RegisterAfterUpsert(gomock.Any())
|
||||
store.EXPECT().RegisterAfterDelete(gomock.Any())
|
||||
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, fmt.Errorf("error"))
|
||||
|
||||
_, err := NewListOptionIndexer(fields, store, false)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewListOptionIndexer() with error from Exec() when creating fields table, should return an error", test: func(t *testing.T) {
|
||||
txClient := NewMockTXClient(gomock.NewController(t))
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
fields := [][]string{{"something"}}
|
||||
id := "somename"
|
||||
stmt := &sql.Stmt{}
|
||||
// logic for NewIndexer(), only interested in if this results in error or not
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
store.EXPECT().GetName().Return(id).AnyTimes()
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
store.EXPECT().RegisterAfterUpsert(gomock.Any())
|
||||
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
|
||||
// end NewIndexer() logic
|
||||
|
||||
store.EXPECT().RegisterAfterUpsert(gomock.Any())
|
||||
store.EXPECT().RegisterAfterDelete(gomock.Any())
|
||||
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsTableFmt, id, `"metadata.name" TEXT, "metadata.creationTimestamp" TEXT, "metadata.namespace" TEXT, "something" TEXT`)).Return(nil)
|
||||
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.name", id, "metadata.name")).Return(fmt.Errorf("error"))
|
||||
|
||||
_, err := NewListOptionIndexer(fields, store, true)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewListOptionIndexer() with error from Commit(), should return an error", test: func(t *testing.T) {
|
||||
txClient := NewMockTXClient(gomock.NewController(t))
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
fields := [][]string{{"something"}}
|
||||
id := "somename"
|
||||
stmt := &sql.Stmt{}
|
||||
// logic for NewIndexer(), only interested in if this results in error or not
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
store.EXPECT().GetName().Return(id).AnyTimes()
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any(), gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
store.EXPECT().RegisterAfterUpsert(gomock.Any())
|
||||
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
|
||||
// end NewIndexer() logic
|
||||
|
||||
store.EXPECT().RegisterAfterUpsert(gomock.Any())
|
||||
store.EXPECT().RegisterAfterDelete(gomock.Any())
|
||||
|
||||
store.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsTableFmt, id, `"metadata.name" TEXT, "metadata.creationTimestamp" TEXT, "metadata.namespace" TEXT, "something" TEXT`)).Return(nil)
|
||||
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.name", id, "metadata.name")).Return(nil)
|
||||
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.namespace", id, "metadata.namespace")).Return(nil)
|
||||
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.creationTimestamp", id, "metadata.creationTimestamp")).Return(nil)
|
||||
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, fields[0][0], id, fields[0][0])).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(fmt.Errorf("error"))
|
||||
|
||||
_, err := NewListOptionIndexer(fields, store, true)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestListByOptions(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
listOptions ListOptions
|
||||
partitions []partition.Partition
|
||||
ns string
|
||||
expectedCountStmt string
|
||||
expectedCountStmtArgs []any
|
||||
expectedStmt string
|
||||
expectedStmtArgs []any
|
||||
expectedList *unstructured.UnstructuredList
|
||||
returnList []any
|
||||
expectedContToken string
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
testObject := testStoreObject{Id: "something", Val: "a"}
|
||||
unstrTestObjectMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&testObject)
|
||||
assert.Nil(t, err)
|
||||
// unstrTestObject
|
||||
var tests []testCase
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions() with no errors returned, should not return an error",
|
||||
listOptions: ListOptions{},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
returnList: []any{},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{}}, Items: []unstructured.Unstructured{}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions() with an empty filter, should not return an error",
|
||||
listOptions: ListOptions{
|
||||
Filters: []OrFilter{{[]Filter{}}},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{}}, Items: []unstructured.Unstructured{}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with ChunkSize set should set limit in prepared sql.Stmt",
|
||||
listOptions: ListOptions{ChunkSize: 2},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC
|
||||
LIMIT ?`,
|
||||
expectedStmtArgs: []interface{}{2},
|
||||
expectedCountStmt: `SELECT COUNT(*) FROM (SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC )`,
|
||||
expectedCountStmtArgs: []interface{}{},
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with Resume set should set offset in prepared sql.Stmt",
|
||||
listOptions: ListOptions{Resume: "4"},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC
|
||||
OFFSET ?`,
|
||||
expectedStmtArgs: []interface{}{4},
|
||||
expectedCountStmt: `SELECT COUNT(*) FROM (SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC )`,
|
||||
expectedCountStmtArgs: []interface{}{},
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with 1 OrFilter set with 1 filter should select where that filter is true in prepared sql.Stmt",
|
||||
listOptions: ListOptions{Filters: []OrFilter{
|
||||
{
|
||||
[]Filter{
|
||||
{
|
||||
Field: []string{"metadata", "somefield"},
|
||||
Match: "somevalue",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(f."metadata.somefield" LIKE ? ESCAPE '\') AND
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
expectedStmtArgs: []any{"somevalue"},
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with 1 OrFilter set with 1 filter with Op set top NotEq should select where that filter is not true in prepared sql.Stmt",
|
||||
listOptions: ListOptions{Filters: []OrFilter{
|
||||
{
|
||||
[]Filter{
|
||||
{
|
||||
Field: []string{"metadata", "somefield"},
|
||||
Match: "somevalue",
|
||||
Op: NotEq,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(f."metadata.somefield" NOT LIKE ? ESCAPE '\') AND
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
expectedStmtArgs: []any{"somevalue"},
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with 1 OrFilter set with 1 filter with Partial set to true should select where that partial match on that filter's value is true in prepared sql.Stmt",
|
||||
listOptions: ListOptions{Filters: []OrFilter{
|
||||
{
|
||||
[]Filter{
|
||||
{
|
||||
Field: []string{"metadata", "somefield"},
|
||||
Match: "somevalue",
|
||||
Partial: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(f."metadata.somefield" LIKE ? ESCAPE '\') AND
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
expectedStmtArgs: []any{"%somevalue%"},
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with 1 OrFilter set with multiple filters should select where any of those filters are true in prepared sql.Stmt",
|
||||
listOptions: ListOptions{Filters: []OrFilter{
|
||||
{
|
||||
[]Filter{
|
||||
{
|
||||
Field: []string{"metadata", "somefield"},
|
||||
Match: "somevalue",
|
||||
Partial: true,
|
||||
},
|
||||
{
|
||||
Field: []string{"metadata", "somefield"},
|
||||
Match: "someothervalue",
|
||||
},
|
||||
{
|
||||
Field: []string{"metadata", "somefield"},
|
||||
Match: "somethirdvalue",
|
||||
Op: NotEq,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(f."metadata.somefield" LIKE ? ESCAPE '\' OR f."metadata.somefield" LIKE ? ESCAPE '\' OR f."metadata.somefield" NOT LIKE ? ESCAPE '\') AND
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
expectedStmtArgs: []any{"%somevalue%", "someothervalue", "somethirdvalue"},
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with multiple OrFilters set should select where all OrFilters contain one filter that is true in prepared sql.Stmt",
|
||||
listOptions: ListOptions{Filters: []OrFilter{
|
||||
{
|
||||
Filters: []Filter{
|
||||
{
|
||||
Field: []string{"metadata", "somefield"},
|
||||
Match: "somevalue",
|
||||
Partial: true,
|
||||
},
|
||||
{
|
||||
Field: []string{"status", "someotherfield"},
|
||||
Match: "someothervalue",
|
||||
Op: NotEq,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Filters: []Filter{
|
||||
{
|
||||
Field: []string{"metadata", "somefield"},
|
||||
Match: "somethirdvalue",
|
||||
Op: Eq,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "test4",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(f."metadata.somefield" LIKE ? ESCAPE '\' OR f."status.someotherfield" NOT LIKE ? ESCAPE '\') AND
|
||||
(f."metadata.somefield" LIKE ? ESCAPE '\') AND
|
||||
(f."metadata.namespace" = ?) AND
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
expectedStmtArgs: []any{"%somevalue%", "someothervalue", "somethirdvalue", "test4"},
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with Sort.PrimaryField set only should sort on that field only, in ascending order in prepared sql.Stmt",
|
||||
listOptions: ListOptions{
|
||||
Sort: Sort{
|
||||
PrimaryField: []string{"metadata", "somefield"},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "test5",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(f."metadata.namespace" = ?) AND
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.somefield" ASC`,
|
||||
expectedStmtArgs: []any{"test5"},
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with Sort.SecondaryField set only should sort on that field only, in ascending order in prepared sql.Stmt",
|
||||
listOptions: ListOptions{
|
||||
Sort: Sort{
|
||||
SecondaryField: []string{"metadata", "somefield"},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.somefield" ASC`,
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with Sort.PrimaryField and Sort.SecondaryField set should sort on PrimaryField in ascending order first and then sort on SecondaryField in ascending order in prepared sql.Stmt",
|
||||
listOptions: ListOptions{
|
||||
Sort: Sort{
|
||||
PrimaryField: []string{"metadata", "somefield"},
|
||||
SecondaryField: []string{"status", "someotherfield"},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.somefield" ASC, f."status.someotherfield" ASC`,
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with Sort.PrimaryField and Sort.SecondaryField set and PrimaryOrder set to DESC should sort on PrimaryField in descending order first and then sort on SecondaryField in ascending order in prepared sql.Stmt",
|
||||
listOptions: ListOptions{
|
||||
Sort: Sort{
|
||||
PrimaryField: []string{"metadata", "somefield"},
|
||||
SecondaryField: []string{"status", "someotherfield"},
|
||||
PrimaryOrder: DESC,
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.somefield" DESC, f."status.someotherfield" ASC`,
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with Sort.SecondaryField set and Sort.PrimaryOrder set to descending should sort on that SecondaryField in ascending order only and ignore PrimaryOrder in prepared sql.Stmt",
|
||||
listOptions: ListOptions{
|
||||
Sort: Sort{
|
||||
SecondaryField: []string{"status", "someotherfield"},
|
||||
PrimaryOrder: DESC,
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."status.someotherfield" ASC`,
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with Sort.PrimaryOrder set only should sort on default primary and secondary fields in ascending order in prepared sql.Stmt",
|
||||
listOptions: ListOptions{
|
||||
Sort: Sort{
|
||||
PrimaryOrder: DESC,
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with Pagination.PageSize set should set limit to PageSize in prepared sql.Stmt",
|
||||
listOptions: ListOptions{
|
||||
Pagination: Pagination{
|
||||
PageSize: 10,
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC
|
||||
LIMIT ?`,
|
||||
expectedStmtArgs: []any{10},
|
||||
expectedCountStmt: `SELECT COUNT(*) FROM (SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC )`,
|
||||
expectedCountStmtArgs: []interface{}{},
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with Pagination.Page and no PageSize set should not add anything to prepared sql.Stmt",
|
||||
listOptions: ListOptions{
|
||||
Pagination: Pagination{
|
||||
Page: 2,
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with Pagination.Page and PageSize set limit to PageSize and offset to PageSize * (Page - 1) in prepared sql.Stmt",
|
||||
listOptions: ListOptions{
|
||||
Pagination: Pagination{
|
||||
PageSize: 10,
|
||||
Page: 2,
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC
|
||||
LIMIT ?
|
||||
OFFSET ?`,
|
||||
expectedStmtArgs: []any{10, 10},
|
||||
|
||||
expectedCountStmt: `SELECT COUNT(*) FROM (SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC )`,
|
||||
expectedCountStmtArgs: []interface{}{},
|
||||
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with a Namespace Partition should select only items where metadata.namespace is equal to Namespace and all other conditions are met in prepared sql.Stmt",
|
||||
partitions: []partition.Partition{
|
||||
{
|
||||
Namespace: "somens",
|
||||
},
|
||||
},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(f."metadata.namespace" = ? AND FALSE)
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
expectedStmtArgs: []any{"somens"},
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with a All Partition should select all items that meet all other conditions in prepared sql.Stmt",
|
||||
partitions: []partition.Partition{
|
||||
{
|
||||
All: true,
|
||||
},
|
||||
},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with a Passthrough Partition should select all items that meet all other conditions prepared sql.Stmt",
|
||||
partitions: []partition.Partition{
|
||||
{
|
||||
Passthrough: true,
|
||||
},
|
||||
},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "ListByOptions with a Names Partition should select only items where metadata.name equals an items in Names and all other conditions are met in prepared sql.Stmt",
|
||||
partitions: []partition.Partition{
|
||||
{
|
||||
Names: sets.New[string]("someid", "someotherid"),
|
||||
},
|
||||
},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(f."metadata.name" IN (?, ?))
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
expectedStmtArgs: []any{"someid", "someotherid"},
|
||||
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
|
||||
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
txClient := NewMockTXClient(gomock.NewController(t))
|
||||
store := NewMockStore(gomock.NewController(t))
|
||||
stmts := NewMockStmt(gomock.NewController(t))
|
||||
i := &Indexer{
|
||||
Store: store,
|
||||
}
|
||||
lii := &ListOptionIndexer{
|
||||
Indexer: i,
|
||||
indexedFields: []string{"metadata.somefield", "status.someotherfield"},
|
||||
}
|
||||
stmt := &sql.Stmt{}
|
||||
rows := &sql.Rows{}
|
||||
objType := reflect.TypeOf(testObject)
|
||||
store.EXPECT().BeginTx(gomock.Any(), false).Return(txClient, nil)
|
||||
txClient.EXPECT().Stmt(gomock.Any()).Return(stmts).AnyTimes()
|
||||
store.EXPECT().GetName().Return("something").AnyTimes()
|
||||
store.EXPECT().Prepare(test.expectedStmt).Do(func(a ...any) {
|
||||
fmt.Println(a)
|
||||
}).Return(stmt)
|
||||
if args := test.expectedStmtArgs; args != nil {
|
||||
stmts.EXPECT().QueryContext(gomock.Any(), gomock.Any()).Return(rows, nil).AnyTimes()
|
||||
} else if strings.Contains(test.expectedStmt, "LIMIT") {
|
||||
stmts.EXPECT().QueryContext(gomock.Any(), args...).Return(rows, nil)
|
||||
txClient.EXPECT().Stmt(gomock.Any()).Return(stmts)
|
||||
stmts.EXPECT().QueryContext(gomock.Any()).Return(rows, nil)
|
||||
} else {
|
||||
stmts.EXPECT().QueryContext(gomock.Any()).Return(rows, nil)
|
||||
}
|
||||
store.EXPECT().GetType().Return(objType)
|
||||
store.EXPECT().GetShouldEncrypt().Return(false)
|
||||
store.EXPECT().ReadObjects(rows, objType, false).Return(test.returnList, nil)
|
||||
store.EXPECT().CloseStmt(stmt).Return(nil)
|
||||
|
||||
if test.expectedCountStmt != "" {
|
||||
store.EXPECT().Prepare(test.expectedCountStmt).Return(stmt)
|
||||
//store.EXPECT().QueryForRows(context.TODO(), stmt, test.expectedCountStmtArgs...).Return(rows, nil)
|
||||
store.EXPECT().ReadInt(rows).Return(len(test.expectedList.Items), nil)
|
||||
store.EXPECT().CloseStmt(stmt).Return(nil)
|
||||
}
|
||||
txClient.EXPECT().Commit()
|
||||
list, total, contToken, err := lii.ListByOptions(context.TODO(), test.listOptions, test.partitions, test.ns)
|
||||
if test.expectedErr == nil {
|
||||
assert.Nil(t, err)
|
||||
} else {
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
}
|
||||
assert.Equal(t, test.expectedList, list)
|
||||
assert.Equal(t, len(test.expectedList.Items), total)
|
||||
assert.Equal(t, test.expectedContToken, contToken)
|
||||
})
|
||||
}
|
||||
}
|
59
pkg/sqlcache/informer/listoptions.go
Normal file
59
pkg/sqlcache/informer/listoptions.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package informer
|
||||
|
||||
type Op string
|
||||
|
||||
const (
|
||||
Eq Op = ""
|
||||
NotEq Op = "!="
|
||||
)
|
||||
|
||||
// SortOrder represents whether the list should be ascending or descending.
|
||||
type SortOrder int
|
||||
|
||||
const (
|
||||
// ASC stands for ascending order.
|
||||
ASC SortOrder = iota
|
||||
// DESC stands for descending (reverse) order.
|
||||
DESC
|
||||
)
|
||||
|
||||
// ListOptions represents the query parameters that may be included in a list request.
|
||||
type ListOptions struct {
|
||||
ChunkSize int
|
||||
Resume string
|
||||
Filters []OrFilter
|
||||
Sort Sort
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
// Filter represents a field to filter by.
|
||||
// A subfield in an object is represented in a request query using . notation, e.g. 'metadata.name'.
|
||||
// The subfield is internally represented as a slice, e.g. [metadata, name].
|
||||
type Filter struct {
|
||||
Field []string
|
||||
Match string
|
||||
Op Op
|
||||
Partial bool
|
||||
}
|
||||
|
||||
// OrFilter represents a set of possible fields to filter by, where an item may match any filter in the set to be included in the result.
|
||||
type OrFilter struct {
|
||||
Filters []Filter
|
||||
}
|
||||
|
||||
// Sort represents the criteria to sort on.
|
||||
// The subfield to sort by is represented in a request query using . notation, e.g. 'metadata.name'.
|
||||
// The subfield is internally represented as a slice, e.g. [metadata, name].
|
||||
// The order is represented by prefixing the sort key by '-', e.g. sort=-metadata.name.
|
||||
type Sort struct {
|
||||
PrimaryField []string
|
||||
SecondaryField []string
|
||||
PrimaryOrder SortOrder
|
||||
SecondaryOrder SortOrder
|
||||
}
|
||||
|
||||
// Pagination represents how to return paginated results.
|
||||
type Pagination struct {
|
||||
PageSize int
|
||||
Page int
|
||||
}
|
22
pkg/sqlcache/informer/shared_informer_hack.go
Normal file
22
pkg/sqlcache/informer/shared_informer_hack.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package informer
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// UnsafeSet replaces the passed object's field value with the passed value.
|
||||
func UnsafeSet(object any, field string, value any) {
|
||||
rs := reflect.ValueOf(object).Elem()
|
||||
rf := rs.FieldByName(field)
|
||||
wrf := reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem()
|
||||
wrf.Set(reflect.ValueOf(value))
|
||||
}
|
||||
|
||||
// UnsafeGet returns the value of the passed object's for the passed field.
|
||||
func UnsafeGet(object any, field string) any {
|
||||
rs := reflect.ValueOf(object).Elem()
|
||||
rf := rs.FieldByName(field)
|
||||
wrf := reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem()
|
||||
return wrf.Interface()
|
||||
}
|
325
pkg/sqlcache/informer/shared_informer_test.go
Normal file
325
pkg/sqlcache/informer/shared_informer_test.go
Normal file
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
Copyright 2023 SUSE LLC
|
||||
|
||||
Adapted from client-go, Copyright 2014 The Kubernetes Authors.
|
||||
*/
|
||||
|
||||
package informer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
fcache "k8s.io/client-go/tools/cache/testing"
|
||||
testingclock "k8s.io/utils/clock/testing"
|
||||
)
|
||||
|
||||
type testListener struct {
|
||||
lock sync.RWMutex
|
||||
resyncPeriod time.Duration
|
||||
expectedItemNames sets.Set[string]
|
||||
receivedItemNames []string
|
||||
name string
|
||||
}
|
||||
|
||||
func newTestListener(name string, resyncPeriod time.Duration, expected ...string) *testListener {
|
||||
l := &testListener{
|
||||
resyncPeriod: resyncPeriod,
|
||||
expectedItemNames: sets.New[string](expected...),
|
||||
name: name,
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *testListener) OnAdd(obj interface{}, isInInitialList bool) {
|
||||
l.handle(obj)
|
||||
}
|
||||
|
||||
func (l *testListener) OnUpdate(old, new interface{}) {
|
||||
l.handle(new)
|
||||
}
|
||||
|
||||
func (l *testListener) OnDelete(obj interface{}) {
|
||||
}
|
||||
|
||||
func (l *testListener) handle(obj interface{}) {
|
||||
key, _ := cache.MetaNamespaceKeyFunc(obj)
|
||||
fmt.Printf("%s: handle: %v\n", l.name, key)
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
objectMeta, _ := meta.Accessor(obj)
|
||||
l.receivedItemNames = append(l.receivedItemNames, objectMeta.GetName())
|
||||
}
|
||||
|
||||
func (l *testListener) ok() bool {
|
||||
fmt.Println("polling")
|
||||
err := wait.PollImmediate(100*time.Millisecond, 2*time.Second, func() (bool, error) {
|
||||
if l.satisfiedExpectations() {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// wait just a bit to allow any unexpected stragglers to come in
|
||||
fmt.Println("sleeping")
|
||||
time.Sleep(1 * time.Second)
|
||||
fmt.Println("final check")
|
||||
return l.satisfiedExpectations()
|
||||
}
|
||||
|
||||
func (l *testListener) satisfiedExpectations() bool {
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
|
||||
return sets.New[string](l.receivedItemNames...).Equal(l.expectedItemNames)
|
||||
}
|
||||
|
||||
func TestListenerResyncPeriods(t *testing.T) {
|
||||
// source simulates an apiserver object endpoint.
|
||||
source := fcache.NewFakeControllerSource()
|
||||
source.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}})
|
||||
source.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod2"}})
|
||||
|
||||
// create the shared informer and resync every 1s
|
||||
informer := cache.NewSharedInformer(source, &v1.Pod{}, 1*time.Second)
|
||||
|
||||
clock := testingclock.NewFakeClock(time.Now())
|
||||
UnsafeSet(informer, "clock", clock)
|
||||
UnsafeSet(UnsafeGet(informer, "processor"), "clock", clock)
|
||||
|
||||
// listener 1, never resync
|
||||
listener1 := newTestListener("listener1", 0, "pod1", "pod2")
|
||||
informer.AddEventHandlerWithResyncPeriod(listener1, listener1.resyncPeriod)
|
||||
|
||||
// listener 2, resync every 2s
|
||||
listener2 := newTestListener("listener2", 2*time.Second, "pod1", "pod2")
|
||||
informer.AddEventHandlerWithResyncPeriod(listener2, listener2.resyncPeriod)
|
||||
|
||||
// listener 3, resync every 3s
|
||||
listener3 := newTestListener("listener3", 3*time.Second, "pod1", "pod2")
|
||||
informer.AddEventHandlerWithResyncPeriod(listener3, listener3.resyncPeriod)
|
||||
listeners := []*testListener{listener1, listener2, listener3}
|
||||
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
|
||||
go informer.Run(stop)
|
||||
|
||||
// ensure all listeners got the initial List
|
||||
for _, listener := range listeners {
|
||||
if !listener.ok() {
|
||||
t.Errorf("%s: expected %v, got %v", listener.name, listener.expectedItemNames, listener.receivedItemNames)
|
||||
}
|
||||
}
|
||||
|
||||
// reset
|
||||
for _, listener := range listeners {
|
||||
listener.receivedItemNames = []string{}
|
||||
}
|
||||
|
||||
// advance so listener2 gets a resync
|
||||
clock.Step(2 * time.Second)
|
||||
|
||||
// make sure listener2 got the resync
|
||||
if !listener2.ok() {
|
||||
t.Errorf("%s: expected %v, got %v", listener2.name, listener2.expectedItemNames, listener2.receivedItemNames)
|
||||
}
|
||||
|
||||
// wait a bit to give errant items a chance to go to 1 and 3
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// make sure listeners 1 and 3 got nothing
|
||||
if len(listener1.receivedItemNames) != 0 {
|
||||
t.Errorf("listener1: should not have resynced (got %d)", len(listener1.receivedItemNames))
|
||||
}
|
||||
if len(listener3.receivedItemNames) != 0 {
|
||||
t.Errorf("listener3: should not have resynced (got %d)", len(listener3.receivedItemNames))
|
||||
}
|
||||
|
||||
// reset
|
||||
for _, listener := range listeners {
|
||||
listener.receivedItemNames = []string{}
|
||||
}
|
||||
|
||||
// advance so listener3 gets a resync
|
||||
clock.Step(1 * time.Second)
|
||||
|
||||
// make sure listener3 got the resync
|
||||
if !listener3.ok() {
|
||||
t.Errorf("%s: expected %v, got %v", listener3.name, listener3.expectedItemNames, listener3.receivedItemNames)
|
||||
}
|
||||
|
||||
// wait a bit to give errant items a chance to go to 1 and 2
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// make sure listeners 1 and 2 got nothing
|
||||
if len(listener1.receivedItemNames) != 0 {
|
||||
t.Errorf("listener1: should not have resynced (got %d)", len(listener1.receivedItemNames))
|
||||
}
|
||||
if len(listener2.receivedItemNames) != 0 {
|
||||
t.Errorf("listener2: should not have resynced (got %d)", len(listener2.receivedItemNames))
|
||||
}
|
||||
}
|
||||
|
||||
// verify that https://github.com/kubernetes/kubernetes/issues/59822 is fixed
|
||||
func TestSharedInformerInitializationRace(t *testing.T) {
|
||||
source := fcache.NewFakeControllerSource()
|
||||
informer := cache.NewSharedInformer(source, &v1.Pod{}, 1*time.Second)
|
||||
listener := newTestListener("raceListener", 0)
|
||||
|
||||
stop := make(chan struct{})
|
||||
go informer.AddEventHandlerWithResyncPeriod(listener, listener.resyncPeriod)
|
||||
go informer.Run(stop)
|
||||
close(stop)
|
||||
}
|
||||
|
||||
// TestSharedInformerWatchDisruption simulates a watch that was closed
|
||||
// with updates to the store during that time. We ensure that handlers with
|
||||
// resync and no resync see the expected state.
|
||||
func TestSharedInformerWatchDisruption(t *testing.T) {
|
||||
// source simulates an apiserver object endpoint.
|
||||
source := fcache.NewFakeControllerSource()
|
||||
|
||||
source.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod1", UID: "pod1", ResourceVersion: "1"}})
|
||||
source.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod2", UID: "pod2", ResourceVersion: "2"}})
|
||||
|
||||
// create the shared informer and resync every 1s
|
||||
informer := cache.NewSharedInformer(source, &v1.Pod{}, 1*time.Second)
|
||||
|
||||
clock := testingclock.NewFakeClock(time.Now())
|
||||
UnsafeSet(informer, "clock", clock)
|
||||
UnsafeSet(UnsafeGet(informer, "processor"), "clock", clock)
|
||||
|
||||
// listener, never resync
|
||||
listenerNoResync := newTestListener("listenerNoResync", 0, "pod1", "pod2")
|
||||
informer.AddEventHandlerWithResyncPeriod(listenerNoResync, listenerNoResync.resyncPeriod)
|
||||
|
||||
listenerResync := newTestListener("listenerResync", 1*time.Second, "pod1", "pod2")
|
||||
informer.AddEventHandlerWithResyncPeriod(listenerResync, listenerResync.resyncPeriod)
|
||||
listeners := []*testListener{listenerNoResync, listenerResync}
|
||||
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
|
||||
go informer.Run(stop)
|
||||
|
||||
for _, listener := range listeners {
|
||||
if !listener.ok() {
|
||||
t.Errorf("%s: expected %v, got %v", listener.name, listener.expectedItemNames, listener.receivedItemNames)
|
||||
}
|
||||
}
|
||||
|
||||
// Add pod3, bump pod2 but don't broadcast it, so that the change will be seen only on relist
|
||||
source.AddDropWatch(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod3", UID: "pod3", ResourceVersion: "3"}})
|
||||
source.ModifyDropWatch(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod2", UID: "pod2", ResourceVersion: "4"}})
|
||||
|
||||
// Ensure that nobody saw any changes
|
||||
for _, listener := range listeners {
|
||||
if !listener.ok() {
|
||||
t.Errorf("%s: expected %v, got %v", listener.name, listener.expectedItemNames, listener.receivedItemNames)
|
||||
}
|
||||
}
|
||||
|
||||
for _, listener := range listeners {
|
||||
listener.receivedItemNames = []string{}
|
||||
}
|
||||
|
||||
listenerNoResync.expectedItemNames = sets.New[string]("pod2", "pod3")
|
||||
listenerResync.expectedItemNames = sets.New[string]("pod1", "pod2", "pod3")
|
||||
|
||||
// This calls shouldSync, which deletes noResync from the list of syncingListeners
|
||||
clock.Step(1 * time.Second)
|
||||
|
||||
// Simulate a connection loss (or even just a too-old-watch)
|
||||
source.ResetWatch()
|
||||
|
||||
// Wait long enough for the reflector to exit and the backoff function to start waiting
|
||||
// on the fake clock, otherwise advancing the fake clock will have no effect.
|
||||
// TODO: Make this deterministic by counting the number of waiters on FakeClock
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Advance the clock to cause the backoff wait to expire.
|
||||
clock.Step(1601 * time.Millisecond)
|
||||
|
||||
// Wait long enough for backoff to invoke ListWatch a second time and distribute events
|
||||
// to listeners.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
for _, listener := range listeners {
|
||||
if !listener.ok() {
|
||||
t.Errorf("%s: expected %v, got %v", listener.name, listener.expectedItemNames, listener.receivedItemNames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedInformerErrorHandling(t *testing.T) {
|
||||
source := fcache.NewFakeControllerSource()
|
||||
source.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}})
|
||||
source.ListError = fmt.Errorf("Access Denied")
|
||||
|
||||
informer := cache.NewSharedInformer(source, &v1.Pod{}, 1*time.Second)
|
||||
|
||||
errCh := make(chan error)
|
||||
_ = informer.SetWatchErrorHandler(func(_ *cache.Reflector, err error) {
|
||||
errCh <- err
|
||||
})
|
||||
|
||||
stop := make(chan struct{})
|
||||
go informer.Run(stop)
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if !strings.Contains(err.Error(), "Access Denied") {
|
||||
t.Errorf("Expected 'Access Denied' error. Actual: %v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("Timeout waiting for error handler call")
|
||||
}
|
||||
close(stop)
|
||||
}
|
||||
|
||||
func TestSharedInformerTransformer(t *testing.T) {
|
||||
// source simulates an apiserver object endpoint.
|
||||
source := fcache.NewFakeControllerSource()
|
||||
|
||||
source.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod1", UID: "pod1", ResourceVersion: "1"}})
|
||||
source.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod2", UID: "pod2", ResourceVersion: "2"}})
|
||||
|
||||
informer := cache.NewSharedInformer(source, &v1.Pod{}, 1*time.Second)
|
||||
informer.SetTransform(func(obj interface{}) (interface{}, error) {
|
||||
if pod, ok := obj.(*v1.Pod); ok {
|
||||
name := pod.GetName()
|
||||
|
||||
if upper := strings.ToUpper(name); upper != name {
|
||||
copied := pod.DeepCopyObject().(*v1.Pod)
|
||||
copied.SetName(upper)
|
||||
return copied, nil
|
||||
}
|
||||
}
|
||||
return obj, nil
|
||||
})
|
||||
|
||||
listenerTransformer := newTestListener("listenerTransformer", 0, "POD1", "POD2")
|
||||
informer.AddEventHandler(listenerTransformer)
|
||||
|
||||
stop := make(chan struct{})
|
||||
go informer.Run(stop)
|
||||
defer close(stop)
|
||||
|
||||
if !listenerTransformer.ok() {
|
||||
t.Errorf("%s: expected %v, got %v", listenerTransformer.name, listenerTransformer.expectedItemNames, listenerTransformer.receivedItemNames)
|
||||
}
|
||||
}
|
347
pkg/sqlcache/informer/sql_mocks_test.go
Normal file
347
pkg/sqlcache/informer/sql_mocks_test.go
Normal file
@@ -0,0 +1,347 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/rancher/steve/pkg/sqlcache/informer (interfaces: Store)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen --build_flags=--mod=mod -package informer -destination ./sql_mocks_test.go github.com/rancher/steve/pkg/sqlcache/informer Store
|
||||
//
|
||||
|
||||
// Package informer is a generated GoMock package.
|
||||
package informer
|
||||
|
||||
import (
|
||||
context "context"
|
||||
sql "database/sql"
|
||||
reflect "reflect"
|
||||
|
||||
db "github.com/rancher/steve/pkg/sqlcache/db"
|
||||
transaction "github.com/rancher/steve/pkg/sqlcache/db/transaction"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockStore is a mock of Store interface.
|
||||
type MockStore struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockStoreMockRecorder
|
||||
}
|
||||
|
||||
// MockStoreMockRecorder is the mock recorder for MockStore.
|
||||
type MockStoreMockRecorder struct {
|
||||
mock *MockStore
|
||||
}
|
||||
|
||||
// NewMockStore creates a new mock instance.
|
||||
func NewMockStore(ctrl *gomock.Controller) *MockStore {
|
||||
mock := &MockStore{ctrl: ctrl}
|
||||
mock.recorder = &MockStoreMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockStore) EXPECT() *MockStoreMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Add mocks base method.
|
||||
func (m *MockStore) Add(arg0 any) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Add", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Add indicates an expected call of Add.
|
||||
func (mr *MockStoreMockRecorder) Add(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockStore)(nil).Add), arg0)
|
||||
}
|
||||
|
||||
// BeginTx mocks base method.
|
||||
func (m *MockStore) BeginTx(arg0 context.Context, arg1 bool) (db.TXClient, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "BeginTx", arg0, arg1)
|
||||
ret0, _ := ret[0].(db.TXClient)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// BeginTx indicates an expected call of BeginTx.
|
||||
func (mr *MockStoreMockRecorder) BeginTx(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTx", reflect.TypeOf((*MockStore)(nil).BeginTx), arg0, arg1)
|
||||
}
|
||||
|
||||
// CloseStmt mocks base method.
|
||||
func (m *MockStore) CloseStmt(arg0 db.Closable) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CloseStmt", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CloseStmt indicates an expected call of CloseStmt.
|
||||
func (mr *MockStoreMockRecorder) CloseStmt(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseStmt", reflect.TypeOf((*MockStore)(nil).CloseStmt), arg0)
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockStore) Delete(arg0 any) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Delete", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockStoreMockRecorder) Delete(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockStore)(nil).Delete), arg0)
|
||||
}
|
||||
|
||||
// Get mocks base method.
|
||||
func (m *MockStore) Get(arg0 any) (any, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Get", arg0)
|
||||
ret0, _ := ret[0].(any)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get.
|
||||
func (mr *MockStoreMockRecorder) Get(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStore)(nil).Get), arg0)
|
||||
}
|
||||
|
||||
// GetByKey mocks base method.
|
||||
func (m *MockStore) GetByKey(arg0 string) (any, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetByKey", arg0)
|
||||
ret0, _ := ret[0].(any)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// GetByKey indicates an expected call of GetByKey.
|
||||
func (mr *MockStoreMockRecorder) GetByKey(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByKey", reflect.TypeOf((*MockStore)(nil).GetByKey), arg0)
|
||||
}
|
||||
|
||||
// GetName mocks base method.
|
||||
func (m *MockStore) GetName() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetName")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetName indicates an expected call of GetName.
|
||||
func (mr *MockStoreMockRecorder) GetName() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockStore)(nil).GetName))
|
||||
}
|
||||
|
||||
// GetShouldEncrypt mocks base method.
|
||||
func (m *MockStore) GetShouldEncrypt() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetShouldEncrypt")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetShouldEncrypt indicates an expected call of GetShouldEncrypt.
|
||||
func (mr *MockStoreMockRecorder) GetShouldEncrypt() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetShouldEncrypt", reflect.TypeOf((*MockStore)(nil).GetShouldEncrypt))
|
||||
}
|
||||
|
||||
// GetType mocks base method.
|
||||
func (m *MockStore) GetType() reflect.Type {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetType")
|
||||
ret0, _ := ret[0].(reflect.Type)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetType indicates an expected call of GetType.
|
||||
func (mr *MockStoreMockRecorder) GetType() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetType", reflect.TypeOf((*MockStore)(nil).GetType))
|
||||
}
|
||||
|
||||
// List mocks base method.
|
||||
func (m *MockStore) List() []any {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "List")
|
||||
ret0, _ := ret[0].([]any)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// List indicates an expected call of List.
|
||||
func (mr *MockStoreMockRecorder) List() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockStore)(nil).List))
|
||||
}
|
||||
|
||||
// ListKeys mocks base method.
|
||||
func (m *MockStore) ListKeys() []string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListKeys")
|
||||
ret0, _ := ret[0].([]string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ListKeys indicates an expected call of ListKeys.
|
||||
func (mr *MockStoreMockRecorder) ListKeys() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListKeys", reflect.TypeOf((*MockStore)(nil).ListKeys))
|
||||
}
|
||||
|
||||
// Prepare mocks base method.
|
||||
func (m *MockStore) Prepare(arg0 string) *sql.Stmt {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Prepare", arg0)
|
||||
ret0, _ := ret[0].(*sql.Stmt)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Prepare indicates an expected call of Prepare.
|
||||
func (mr *MockStoreMockRecorder) Prepare(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prepare", reflect.TypeOf((*MockStore)(nil).Prepare), arg0)
|
||||
}
|
||||
|
||||
// QueryForRows mocks base method.
|
||||
func (m *MockStore) QueryForRows(arg0 context.Context, arg1 transaction.Stmt, arg2 ...any) (*sql.Rows, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1}
|
||||
for _, a := range arg2 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "QueryForRows", varargs...)
|
||||
ret0, _ := ret[0].(*sql.Rows)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// QueryForRows indicates an expected call of QueryForRows.
|
||||
func (mr *MockStoreMockRecorder) QueryForRows(arg0, arg1 any, arg2 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1}, arg2...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryForRows", reflect.TypeOf((*MockStore)(nil).QueryForRows), varargs...)
|
||||
}
|
||||
|
||||
// ReadInt mocks base method.
|
||||
func (m *MockStore) ReadInt(arg0 db.Rows) (int, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadInt", arg0)
|
||||
ret0, _ := ret[0].(int)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadInt indicates an expected call of ReadInt.
|
||||
func (mr *MockStoreMockRecorder) ReadInt(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadInt", reflect.TypeOf((*MockStore)(nil).ReadInt), arg0)
|
||||
}
|
||||
|
||||
// ReadObjects mocks base method.
|
||||
func (m *MockStore) ReadObjects(arg0 db.Rows, arg1 reflect.Type, arg2 bool) ([]any, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadObjects", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].([]any)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadObjects indicates an expected call of ReadObjects.
|
||||
func (mr *MockStoreMockRecorder) ReadObjects(arg0, arg1, arg2 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadObjects", reflect.TypeOf((*MockStore)(nil).ReadObjects), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// ReadStrings mocks base method.
|
||||
func (m *MockStore) ReadStrings(arg0 db.Rows) ([]string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadStrings", arg0)
|
||||
ret0, _ := ret[0].([]string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadStrings indicates an expected call of ReadStrings.
|
||||
func (mr *MockStoreMockRecorder) ReadStrings(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadStrings", reflect.TypeOf((*MockStore)(nil).ReadStrings), arg0)
|
||||
}
|
||||
|
||||
// RegisterAfterDelete mocks base method.
|
||||
func (m *MockStore) RegisterAfterDelete(arg0 func(string, db.TXClient) error) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "RegisterAfterDelete", arg0)
|
||||
}
|
||||
|
||||
// RegisterAfterDelete indicates an expected call of RegisterAfterDelete.
|
||||
func (mr *MockStoreMockRecorder) RegisterAfterDelete(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterAfterDelete", reflect.TypeOf((*MockStore)(nil).RegisterAfterDelete), arg0)
|
||||
}
|
||||
|
||||
// RegisterAfterUpsert mocks base method.
|
||||
func (m *MockStore) RegisterAfterUpsert(arg0 func(string, any, db.TXClient) error) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "RegisterAfterUpsert", arg0)
|
||||
}
|
||||
|
||||
// RegisterAfterUpsert indicates an expected call of RegisterAfterUpsert.
|
||||
func (mr *MockStoreMockRecorder) RegisterAfterUpsert(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterAfterUpsert", reflect.TypeOf((*MockStore)(nil).RegisterAfterUpsert), arg0)
|
||||
}
|
||||
|
||||
// Replace mocks base method.
|
||||
func (m *MockStore) Replace(arg0 []any, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Replace", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Replace indicates an expected call of Replace.
|
||||
func (mr *MockStoreMockRecorder) Replace(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Replace", reflect.TypeOf((*MockStore)(nil).Replace), arg0, arg1)
|
||||
}
|
||||
|
||||
// Resync mocks base method.
|
||||
func (m *MockStore) Resync() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Resync")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Resync indicates an expected call of Resync.
|
||||
func (mr *MockStoreMockRecorder) Resync() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resync", reflect.TypeOf((*MockStore)(nil).Resync))
|
||||
}
|
||||
|
||||
// Update mocks base method.
|
||||
func (m *MockStore) Update(arg0 any) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Update", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Update indicates an expected call of Update.
|
||||
func (mr *MockStoreMockRecorder) Update(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockStore)(nil).Update), arg0)
|
||||
}
|
165
pkg/sqlcache/informer/store_mocks_test.go
Normal file
165
pkg/sqlcache/informer/store_mocks_test.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/rancher/steve/pkg/sqlcache/store (interfaces: DBClient)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen --build_flags=--mod=mod -package informer -destination ./store_mocks_test.go github.com/rancher/steve/pkg/sqlcache/store DBClient
|
||||
//
|
||||
|
||||
// Package informer is a generated GoMock package.
|
||||
package informer
|
||||
|
||||
import (
|
||||
context "context"
|
||||
sql "database/sql"
|
||||
reflect "reflect"
|
||||
|
||||
db "github.com/rancher/steve/pkg/sqlcache/db"
|
||||
transaction "github.com/rancher/steve/pkg/sqlcache/db/transaction"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockDBClient is a mock of DBClient interface.
|
||||
type MockDBClient struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockDBClientMockRecorder
|
||||
}
|
||||
|
||||
// MockDBClientMockRecorder is the mock recorder for MockDBClient.
|
||||
type MockDBClientMockRecorder struct {
|
||||
mock *MockDBClient
|
||||
}
|
||||
|
||||
// NewMockDBClient creates a new mock instance.
|
||||
func NewMockDBClient(ctrl *gomock.Controller) *MockDBClient {
|
||||
mock := &MockDBClient{ctrl: ctrl}
|
||||
mock.recorder = &MockDBClientMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockDBClient) EXPECT() *MockDBClientMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// BeginTx mocks base method.
|
||||
func (m *MockDBClient) BeginTx(arg0 context.Context, arg1 bool) (db.TXClient, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "BeginTx", arg0, arg1)
|
||||
ret0, _ := ret[0].(db.TXClient)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// BeginTx indicates an expected call of BeginTx.
|
||||
func (mr *MockDBClientMockRecorder) BeginTx(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTx", reflect.TypeOf((*MockDBClient)(nil).BeginTx), arg0, arg1)
|
||||
}
|
||||
|
||||
// CloseStmt mocks base method.
|
||||
func (m *MockDBClient) CloseStmt(arg0 db.Closable) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CloseStmt", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CloseStmt indicates an expected call of CloseStmt.
|
||||
func (mr *MockDBClientMockRecorder) CloseStmt(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseStmt", reflect.TypeOf((*MockDBClient)(nil).CloseStmt), arg0)
|
||||
}
|
||||
|
||||
// Prepare mocks base method.
|
||||
func (m *MockDBClient) Prepare(arg0 string) *sql.Stmt {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Prepare", arg0)
|
||||
ret0, _ := ret[0].(*sql.Stmt)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Prepare indicates an expected call of Prepare.
|
||||
func (mr *MockDBClientMockRecorder) Prepare(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prepare", reflect.TypeOf((*MockDBClient)(nil).Prepare), arg0)
|
||||
}
|
||||
|
||||
// QueryForRows mocks base method.
|
||||
func (m *MockDBClient) QueryForRows(arg0 context.Context, arg1 transaction.Stmt, arg2 ...any) (*sql.Rows, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1}
|
||||
for _, a := range arg2 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "QueryForRows", varargs...)
|
||||
ret0, _ := ret[0].(*sql.Rows)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// QueryForRows indicates an expected call of QueryForRows.
|
||||
func (mr *MockDBClientMockRecorder) QueryForRows(arg0, arg1 any, arg2 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0, arg1}, arg2...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryForRows", reflect.TypeOf((*MockDBClient)(nil).QueryForRows), varargs...)
|
||||
}
|
||||
|
||||
// ReadInt mocks base method.
|
||||
func (m *MockDBClient) ReadInt(arg0 db.Rows) (int, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadInt", arg0)
|
||||
ret0, _ := ret[0].(int)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadInt indicates an expected call of ReadInt.
|
||||
func (mr *MockDBClientMockRecorder) ReadInt(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadInt", reflect.TypeOf((*MockDBClient)(nil).ReadInt), arg0)
|
||||
}
|
||||
|
||||
// ReadObjects mocks base method.
|
||||
func (m *MockDBClient) ReadObjects(arg0 db.Rows, arg1 reflect.Type, arg2 bool) ([]any, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadObjects", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].([]any)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadObjects indicates an expected call of ReadObjects.
|
||||
func (mr *MockDBClientMockRecorder) ReadObjects(arg0, arg1, arg2 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadObjects", reflect.TypeOf((*MockDBClient)(nil).ReadObjects), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// ReadStrings mocks base method.
|
||||
func (m *MockDBClient) ReadStrings(arg0 db.Rows) ([]string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadStrings", arg0)
|
||||
ret0, _ := ret[0].([]string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadStrings indicates an expected call of ReadStrings.
|
||||
func (mr *MockDBClientMockRecorder) ReadStrings(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadStrings", reflect.TypeOf((*MockDBClient)(nil).ReadStrings), arg0)
|
||||
}
|
||||
|
||||
// Upsert mocks base method.
|
||||
func (m *MockDBClient) Upsert(arg0 db.TXClient, arg1 *sql.Stmt, arg2 string, arg3 any, arg4 bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Upsert", arg0, arg1, arg2, arg3, arg4)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Upsert indicates an expected call of Upsert.
|
||||
func (mr *MockDBClientMockRecorder) Upsert(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upsert", reflect.TypeOf((*MockDBClient)(nil).Upsert), arg0, arg1, arg2, arg3, arg4)
|
||||
}
|
99
pkg/sqlcache/informer/tx_mocks_test.go
Normal file
99
pkg/sqlcache/informer/tx_mocks_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/rancher/lasso/pkg/cache/sql/db/transaction (interfaces: Stmt)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen --build_flags=--mod=mod -package informer -destination ./pkg/cache/sql/informer/tx_mocks_test.go github.com/rancher/lasso/pkg/cache/sql/db/transaction Stmt
|
||||
//
|
||||
|
||||
// Package informer is a generated GoMock package.
|
||||
package informer
|
||||
|
||||
import (
|
||||
context "context"
|
||||
sql "database/sql"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockStmt is a mock of Stmt interface.
|
||||
type MockStmt struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockStmtMockRecorder
|
||||
}
|
||||
|
||||
// MockStmtMockRecorder is the mock recorder for MockStmt.
|
||||
type MockStmtMockRecorder struct {
|
||||
mock *MockStmt
|
||||
}
|
||||
|
||||
// NewMockStmt creates a new mock instance.
|
||||
func NewMockStmt(ctrl *gomock.Controller) *MockStmt {
|
||||
mock := &MockStmt{ctrl: ctrl}
|
||||
mock.recorder = &MockStmtMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockStmt) EXPECT() *MockStmtMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Exec mocks base method.
|
||||
func (m *MockStmt) Exec(arg0 ...any) (sql.Result, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{}
|
||||
for _, a := range arg0 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Exec", varargs...)
|
||||
ret0, _ := ret[0].(sql.Result)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Exec indicates an expected call of Exec.
|
||||
func (mr *MockStmtMockRecorder) Exec(arg0 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockStmt)(nil).Exec), arg0...)
|
||||
}
|
||||
|
||||
// Query mocks base method.
|
||||
func (m *MockStmt) Query(arg0 ...any) (*sql.Rows, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{}
|
||||
for _, a := range arg0 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Query", varargs...)
|
||||
ret0, _ := ret[0].(*sql.Rows)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Query indicates an expected call of Query.
|
||||
func (mr *MockStmtMockRecorder) Query(arg0 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockStmt)(nil).Query), arg0...)
|
||||
}
|
||||
|
||||
// QueryContext mocks base method.
|
||||
func (m *MockStmt) QueryContext(arg0 context.Context, arg1 ...any) (*sql.Rows, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "QueryContext", varargs...)
|
||||
ret0, _ := ret[0].(*sql.Rows)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// QueryContext indicates an expected call of QueryContext.
|
||||
func (mr *MockStmtMockRecorder) QueryContext(arg0 any, arg1 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryContext", reflect.TypeOf((*MockStmt)(nil).QueryContext), varargs...)
|
||||
}
|
Reference in New Issue
Block a user