1
0
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:
Tom Lebreux
2025-02-04 12:41:59 -05:00
committed by GitHub
parent d50101289f
commit 9741028761
56 changed files with 9999 additions and 32 deletions

View 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...)
}

View 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)
}

View 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...)
}

View 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)
}

View 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)
}

View 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
}

View 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) })
}
}

View 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)
}

View 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
}

View 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) })
}
}

View 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
}

View 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)
}

View 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
}

View 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
}

View 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)
})
}
}

View 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
}

View 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()
}

View 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)
}
}

View 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)
}

View 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)
}

View 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...)
}