mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 21:42:37 +08:00

* Fix dualwrite package not returning error when getting a non-not found error from legacy storage in mode
443 lines
14 KiB
Go
443 lines
14 KiB
Go
package dualwrite
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
"github.com/grafana/grafana/pkg/apiserver/rest"
|
|
)
|
|
|
|
func TestMode3_Create(t *testing.T) {
|
|
type testCase struct {
|
|
input runtime.Object
|
|
setupLegacyFn func(m *mock.Mock, input runtime.Object)
|
|
setupStorageFn func(m *mock.Mock, input runtime.Object)
|
|
name string
|
|
wantErr bool
|
|
}
|
|
tests :=
|
|
[]testCase{
|
|
{
|
|
name: "should succeed when creating an object in both the LegacyStorage and Storage",
|
|
input: exampleObj,
|
|
setupLegacyFn: func(m *mock.Mock, input runtime.Object) {
|
|
m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, nil).Once()
|
|
},
|
|
setupStorageFn: func(m *mock.Mock, _ runtime.Object) {
|
|
// We don't use the input here, as the input is transformed before being passed to unified storage.
|
|
m.On("Create", mock.Anything, exampleObjNoRV, mock.Anything, mock.Anything).Return(exampleObj, nil).Once()
|
|
},
|
|
},
|
|
{
|
|
name: "should return an error when creating an object in the legacy store fails",
|
|
input: failingObj,
|
|
setupLegacyFn: func(m *mock.Mock, input runtime.Object) {
|
|
m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, errors.New("error")).Once()
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "should return an error when creating an object in the unified store fails and delete from LegacyStorage",
|
|
input: exampleObj,
|
|
setupLegacyFn: func(m *mock.Mock, input runtime.Object) {
|
|
m.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, true, nil).Once()
|
|
m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, nil).Once()
|
|
},
|
|
setupStorageFn: func(m *mock.Mock, _ runtime.Object) {
|
|
// We don't use the input here, as the input is transformed before being passed to unified storage.
|
|
m.On("Create", mock.Anything, exampleObjNoRV, mock.Anything, mock.Anything).Return(nil, errors.New("error")).Once()
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
l := (rest.Storage)(nil)
|
|
s := (rest.Storage)(nil)
|
|
|
|
ls := storageMock{&mock.Mock{}, l}
|
|
us := storageMock{&mock.Mock{}, s}
|
|
|
|
if tt.setupLegacyFn != nil {
|
|
tt.setupLegacyFn(ls.Mock, tt.input)
|
|
}
|
|
if tt.setupStorageFn != nil {
|
|
tt.setupStorageFn(us.Mock, tt.input)
|
|
}
|
|
|
|
dw, err := NewDualWriter(kind, rest.Mode3, ls, us)
|
|
require.NoError(t, err)
|
|
|
|
obj, err := dw.Create(context.Background(), tt.input, createFn, &metav1.CreateOptions{})
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
|
|
require.Equal(t, exampleObj, obj)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMode3_Get(t *testing.T) {
|
|
type testCase struct {
|
|
setupLegacyFn func(m *mock.Mock, name string)
|
|
setupStorageFn func(m *mock.Mock, name string)
|
|
name string
|
|
wantErr bool
|
|
}
|
|
tests :=
|
|
[]testCase{
|
|
{
|
|
name: "should succeed when getting an object from both stores",
|
|
setupLegacyFn: func(m *mock.Mock, name string) {
|
|
m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil)
|
|
},
|
|
setupStorageFn: func(m *mock.Mock, name string) {
|
|
m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil)
|
|
},
|
|
},
|
|
{
|
|
name: "should return an error when getting an object in the unified store fails, and should not go to legacy",
|
|
setupStorageFn: func(m *mock.Mock, name string) {
|
|
m.On("Get", mock.Anything, name, mock.Anything).Return(nil, errors.New("error"))
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "should succeed when getting an object in the LegacyStorage fails",
|
|
setupLegacyFn: func(m *mock.Mock, name string) {
|
|
m.On("Get", mock.Anything, name, mock.Anything).Return(nil, errors.New("error"))
|
|
},
|
|
setupStorageFn: func(m *mock.Mock, name string) {
|
|
m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil)
|
|
},
|
|
},
|
|
}
|
|
|
|
name := "foo"
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
l := (rest.Storage)(nil)
|
|
s := (rest.Storage)(nil)
|
|
|
|
ls := storageMock{&mock.Mock{}, l}
|
|
us := storageMock{&mock.Mock{}, s}
|
|
|
|
if tt.setupLegacyFn != nil {
|
|
tt.setupLegacyFn(ls.Mock, name)
|
|
}
|
|
if tt.setupStorageFn != nil {
|
|
tt.setupStorageFn(us.Mock, name)
|
|
}
|
|
|
|
dw, err := NewDualWriter(kind, rest.Mode3, ls, us)
|
|
require.NoError(t, err)
|
|
|
|
obj, err := dw.Get(context.Background(), name, &metav1.GetOptions{})
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
|
|
require.Equal(t, obj, exampleObj)
|
|
require.NotEqual(t, obj, anotherObj)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMode3_List(t *testing.T) {
|
|
type testCase struct {
|
|
setupStorageFn func(m *mock.Mock, options *metainternalversion.ListOptions)
|
|
name string
|
|
wantErr bool
|
|
}
|
|
tests :=
|
|
[]testCase{
|
|
{
|
|
name: "should return an error when listing an object in the UnifiedStorage is failing",
|
|
setupStorageFn: func(m *mock.Mock, options *metainternalversion.ListOptions) {
|
|
m.On("List", mock.Anything, options).Return(nil, errors.New("error"))
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "should succeed when listing objects in the UnifiedStorage is successful",
|
|
setupStorageFn: func(m *mock.Mock, options *metainternalversion.ListOptions) {
|
|
m.On("List", mock.Anything, options).Return(exampleList, nil)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
l := (rest.Storage)(nil)
|
|
s := (rest.Storage)(nil)
|
|
|
|
ls := storageMock{&mock.Mock{}, l}
|
|
us := storageMock{&mock.Mock{}, s}
|
|
|
|
if tt.setupStorageFn != nil {
|
|
tt.setupStorageFn(us.Mock, &metainternalversion.ListOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}})
|
|
}
|
|
|
|
dw, err := NewDualWriter(kind, rest.Mode3, ls, us)
|
|
require.NoError(t, err)
|
|
|
|
res, err := dw.List(context.Background(), &metainternalversion.ListOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}})
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
|
|
require.Equal(t, exampleList, res)
|
|
require.NotEqual(t, anotherList, res)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMode3_Delete(t *testing.T) {
|
|
type testCase struct {
|
|
setupLegacyFn func(m *mock.Mock, input string)
|
|
setupStorageFn func(m *mock.Mock, input string)
|
|
name string
|
|
wantErr bool
|
|
}
|
|
tests :=
|
|
[]testCase{
|
|
{
|
|
name: "should succeed when deleting an object in both stores",
|
|
setupLegacyFn: func(m *mock.Mock, input string) {
|
|
m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil)
|
|
},
|
|
setupStorageFn: func(m *mock.Mock, input string) {
|
|
m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil)
|
|
},
|
|
},
|
|
{
|
|
name: "should succeed when deleting an object in the LegacyStorage is not found, but found in the UnifiedStorage",
|
|
setupLegacyFn: func(m *mock.Mock, input string) {
|
|
m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, input))
|
|
},
|
|
setupStorageFn: func(m *mock.Mock, input string) {
|
|
m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil)
|
|
},
|
|
},
|
|
{
|
|
name: "should succeed when deleting an object in the UnifiedStorage is not found in the LegacyStorage",
|
|
setupLegacyFn: func(m *mock.Mock, input string) {
|
|
m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, input))
|
|
},
|
|
setupStorageFn: func(m *mock.Mock, input string) {
|
|
m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil)
|
|
},
|
|
},
|
|
{
|
|
name: "should return an error when deleting an object in the LegacyStorage and UnifiedStorage is failing",
|
|
setupLegacyFn: func(m *mock.Mock, input string) {
|
|
m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, errors.New("error"))
|
|
},
|
|
setupStorageFn: func(m *mock.Mock, input string) {
|
|
m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, errors.New("error"))
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "should return an error when deleting an object in the LegacyStorage fails",
|
|
setupLegacyFn: func(m *mock.Mock, input string) {
|
|
m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, apierrors.NewInternalError(errors.New("error")))
|
|
},
|
|
setupStorageFn: func(m *mock.Mock, input string) {
|
|
m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Panic("i should not be called")
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
name := "foo"
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
l := (rest.Storage)(nil)
|
|
s := (rest.Storage)(nil)
|
|
|
|
ls := storageMock{&mock.Mock{}, l}
|
|
us := storageMock{&mock.Mock{}, s}
|
|
|
|
if tt.setupLegacyFn != nil {
|
|
tt.setupLegacyFn(ls.Mock, name)
|
|
}
|
|
if tt.setupStorageFn != nil {
|
|
tt.setupStorageFn(us.Mock, name)
|
|
}
|
|
|
|
dw, err := NewDualWriter(kind, rest.Mode3, ls, us)
|
|
require.NoError(t, err)
|
|
|
|
obj, _, err := dw.Delete(context.Background(), name, func(context.Context, runtime.Object) error { return nil }, &metav1.DeleteOptions{})
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
|
|
require.Equal(t, obj, exampleObj)
|
|
require.NotEqual(t, obj, anotherObj)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMode3_DeleteCollection(t *testing.T) {
|
|
type testCase struct {
|
|
setupLegacyFn func(m *mock.Mock)
|
|
setupStorageFn func(m *mock.Mock)
|
|
name string
|
|
wantErr bool
|
|
}
|
|
tests :=
|
|
[]testCase{
|
|
{
|
|
name: "should succeed when deleting a collection in both stores",
|
|
setupLegacyFn: func(m *mock.Mock) {
|
|
m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleList, nil)
|
|
},
|
|
setupStorageFn: func(m *mock.Mock) {
|
|
m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleList, nil)
|
|
},
|
|
},
|
|
{
|
|
name: "should return an error when deleting a collection in the storage fails and LegacyStorage is successful",
|
|
setupLegacyFn: func(m *mock.Mock) {
|
|
m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, nil)
|
|
},
|
|
setupStorageFn: func(m *mock.Mock) {
|
|
m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("error"))
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "should return an error when deleting a collection in the LegacyStorage fails",
|
|
setupLegacyFn: func(m *mock.Mock) {
|
|
m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("error"))
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
name := "foo"
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
l := (rest.Storage)(nil)
|
|
s := (rest.Storage)(nil)
|
|
|
|
ls := storageMock{&mock.Mock{}, l}
|
|
us := storageMock{&mock.Mock{}, s}
|
|
|
|
if tt.setupLegacyFn != nil {
|
|
tt.setupLegacyFn(ls.Mock)
|
|
}
|
|
if tt.setupStorageFn != nil {
|
|
tt.setupStorageFn(us.Mock)
|
|
}
|
|
|
|
dw, err := NewDualWriter(kind, rest.Mode3, ls, us)
|
|
require.NoError(t, err)
|
|
|
|
obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: name}}, &metainternalversion.ListOptions{})
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.Equal(t, exampleList, obj)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMode3_Update(t *testing.T) {
|
|
type testCase struct {
|
|
expectedObj runtime.Object
|
|
setupLegacyFn func(m *mock.Mock, input string)
|
|
setupStorageFn func(m *mock.Mock, input string)
|
|
name string
|
|
wantErr bool
|
|
}
|
|
tests :=
|
|
[]testCase{
|
|
{
|
|
name: "should succeed when updating an object in both stores",
|
|
setupLegacyFn: func(m *mock.Mock, input string) {
|
|
m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil).Once()
|
|
},
|
|
setupStorageFn: func(m *mock.Mock, input string) {
|
|
m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil).Once()
|
|
},
|
|
expectedObj: exampleObj,
|
|
},
|
|
{
|
|
name: "should return an error when updating an object in the LegacyStorage fails",
|
|
setupLegacyFn: func(m *mock.Mock, input string) {
|
|
m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")).Once()
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "should return an error when updating an object in the UnifiedStorage fails",
|
|
setupLegacyFn: func(m *mock.Mock, input string) {
|
|
m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil).Once()
|
|
},
|
|
setupStorageFn: func(m *mock.Mock, input string) {
|
|
m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")).Once()
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
name := "foo"
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
l := (rest.Storage)(nil)
|
|
s := (rest.Storage)(nil)
|
|
|
|
ls := storageMock{&mock.Mock{}, l}
|
|
us := storageMock{&mock.Mock{}, s}
|
|
|
|
if tt.setupLegacyFn != nil {
|
|
tt.setupLegacyFn(ls.Mock, name)
|
|
}
|
|
if tt.setupStorageFn != nil {
|
|
tt.setupStorageFn(us.Mock, name)
|
|
}
|
|
|
|
dw, err := NewDualWriter(kind, rest.Mode3, ls, us)
|
|
require.NoError(t, err)
|
|
|
|
obj, _, err := dw.Update(context.Background(), name, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{})
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
|
|
require.Equal(t, tt.expectedObj, obj)
|
|
require.NotEqual(t, anotherObj, obj)
|
|
})
|
|
}
|
|
}
|