Dual writer: mode 4 (#90581)

* Itroduce watcher and mode4

* Logging

* Mode4 should be initialized from the dual writer for observability

* Comment watch while it's not implemented

* Lint

* Use mode log when dual writer is initiated

* Use error from logger
This commit is contained in:
Leonor Oliveira 2024-07-24 13:10:10 +01:00 committed by GitHub
parent 701e715662
commit ec343f276f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 414 additions and 73 deletions

View File

@ -9,6 +9,7 @@ import (
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/watch"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/klog/v2"
)
@ -25,7 +26,7 @@ const mode1Str = "1"
// NewDualWriterMode1 returns a new DualWriter in mode 1.
// Mode 1 represents writing to and reading from LegacyStorage.
func newDualWriterMode1(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics) *DualWriterMode1 {
return &DualWriterMode1{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode1"), dualWriterMetrics: dwm}
return &DualWriterMode1{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode1").WithValues("mode", mode1Str), dualWriterMetrics: dwm}
}
// Mode returns the mode of the dual writer.
@ -274,6 +275,11 @@ func (d *DualWriterMode1) Destroy() {
d.Legacy.Destroy()
}
func (d *DualWriterMode1) Watch(ctx context.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
d.Log.Error(errors.New("Watch not implemented in mode 1"), "Watch not implemented in mode 1")
return nil, nil
}
func (d *DualWriterMode1) GetSingularName() string {
return d.Legacy.GetSingularName()
}

View File

@ -2,6 +2,7 @@ package rest
import (
"context"
"errors"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@ -9,6 +10,7 @@ import (
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/watch"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/klog/v2"
@ -27,7 +29,7 @@ const mode2Str = "2"
// NewDualWriterMode2 returns a new DualWriter in mode 2.
// Mode 2 represents writing to LegacyStorage and Storage and reading from LegacyStorage.
func newDualWriterMode2(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics) *DualWriterMode2 {
return &DualWriterMode2{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode2"), dualWriterMetrics: dwm}
return &DualWriterMode2{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode2").WithValues("mode", mode2Str), dualWriterMetrics: dwm}
}
// Mode returns the mode of the dual writer.
@ -332,6 +334,11 @@ func (d *DualWriterMode2) Update(ctx context.Context, name string, objInfo rest.
return res, created, err
}
func (d *DualWriterMode2) Watch(ctx context.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
d.Log.Error(errors.New("Watch not implemented in mode 2"), "Watch not implemented in mode 2")
return nil, nil
}
func (d *DualWriterMode2) Destroy() {
d.Storage.Destroy()
d.Legacy.Destroy()

View File

@ -169,6 +169,13 @@ func (d *DualWriterMode3) DeleteCollection(ctx context.Context, deleteValidation
return res, err
}
//TODO: uncomment when storage watch is implemented
// func (d *DualWriterMode3) Watch(ctx context.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
// var method = "watch"
// d.Log.WithValues("kind", options.Kind, "method", method, "mode", mode3Str).Info("starting to watch")
// return d.Storage.Watch(ctx, options)
// }
func (d *DualWriterMode3) Destroy() {
d.Storage.Destroy()
d.Legacy.Destroy()

View File

@ -17,10 +17,12 @@ type DualWriterMode4 struct {
Log klog.Logger
}
const mode4Str = "4"
// newDualWriterMode4 returns a new DualWriter in mode 4.
// Mode 4 represents writing and reading from Storage.
func newDualWriterMode4(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics) *DualWriterMode4 {
return &DualWriterMode4{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode4"), dualWriterMetrics: dwm}
return &DualWriterMode4{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode4").WithValues("mode", mode4Str), dualWriterMetrics: dwm}
}
// Mode returns the mode of the dual writer.
@ -32,32 +34,81 @@ func (d *DualWriterMode4) Mode() DualWriterMode {
// Create overrides the behavior of the generic DualWriter and writes only to Storage.
func (d *DualWriterMode4) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
return d.Storage.Create(ctx, obj, createValidation, options)
var method = "create"
log := d.Log.WithValues("kind", options.Kind, "method", method)
ctx = klog.NewContext(ctx, log)
res, err := d.Storage.Create(ctx, obj, createValidation, options)
if err != nil {
log.Error(err, "unable to create object in storage")
}
return res, err
}
// Get overrides the behavior of the generic DualWriter and retrieves an object from Storage.
func (d *DualWriterMode4) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return d.Storage.Get(ctx, name, &metav1.GetOptions{})
var method = "get"
log := d.Log.WithValues("kind", options.Kind, "method", method)
ctx = klog.NewContext(ctx, log)
res, err := d.Storage.Get(ctx, name, options)
if err != nil {
log.Error(err, "unable to create object in storage")
}
return res, err
}
func (d *DualWriterMode4) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
return d.Storage.Delete(ctx, name, deleteValidation, options)
var method = "delete"
log := d.Log.WithValues("name", name, "kind", options.Kind, "method", method)
ctx = klog.NewContext(ctx, log)
res, async, err := d.Storage.Delete(ctx, name, deleteValidation, options)
if err != nil {
log.Error(err, "unable to delete object in storage")
}
return res, async, err
}
// DeleteCollection overrides the behavior of the generic DualWriter and deletes only from Storage.
func (d *DualWriterMode4) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) {
return d.Storage.DeleteCollection(ctx, deleteValidation, options, listOptions)
var method = "delete-collection"
log := d.Log.WithValues("kind", options.Kind, "resourceVersion", listOptions.ResourceVersion, "method", method, "mode", mode4Str)
ctx = klog.NewContext(ctx, log)
res, err := d.Storage.DeleteCollection(ctx, deleteValidation, options, listOptions)
if err != nil {
log.Error(err, "unable to delete collection in storage")
}
return res, err
}
// Update overrides the generic behavior of the Storage and writes only to US.
func (d *DualWriterMode4) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
return d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
var method = "update"
log := d.Log.WithValues("name", name, "kind", options.Kind, "method", method)
ctx = klog.NewContext(ctx, log)
res, async, err := d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
if err != nil {
log.Error(err, "unable to update object in storage")
}
return res, async, err
}
func (d *DualWriterMode4) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
return d.Storage.List(ctx, options)
var method = "list"
log := d.Log.WithValues("kind", options.Kind, "resourceVersion", options.ResourceVersion, "kind", options.Kind, "method", method)
ctx = klog.NewContext(ctx, log)
res, err := d.Storage.List(ctx, options)
if err != nil {
log.Error(err, "unable to list objects in storage")
}
return res, err
}
//TODO: uncomment when storage watch is implemented
// func (d *DualWriterMode4) Watch(ctx context.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
// var method = "watch"
// d.Log.WithValues("kind", options.Kind, "method", method, "mode", mode4Str).Info("starting to watch")
// return d.Storage.Watch(ctx, options)
// }
func (d *DualWriterMode4) Destroy() {
d.Storage.Destroy()
}

View File

@ -1,72 +1,346 @@
package rest
// import (
// "context"
// "testing"
import (
"context"
"errors"
"testing"
// "github.com/stretchr/testify/assert"
// metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// "k8s.io/apimachinery/pkg/runtime"
// "k8s.io/apiserver/pkg/apis/example"
// )
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"k8s.io/apimachinery/pkg/api/meta"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// func TestMode4(t *testing.T) {
// var ls = (LegacyStorage)(nil)
// var s = (Storage)(nil)
// lsSpy := NewLegacyStorageSpyClient(ls)
// sSpy := NewStorageSpyClient(s)
func TestMode4_Create(t *testing.T) {
type testCase struct {
input runtime.Object
setupStorageFn func(m *mock.Mock, input runtime.Object)
name string
wantErr bool
}
tests :=
[]testCase{
{
name: "creating an object only in the unified store",
input: exampleObj,
setupStorageFn: func(m *mock.Mock, input runtime.Object) {
m.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, nil)
},
},
{
name: "error when creating object in the unified store fails",
input: failingObj,
setupStorageFn: func(m *mock.Mock, input runtime.Object) {
m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, errors.New("error"))
},
wantErr: true,
},
}
// dw := NewDualWriterMode4(lsSpy, sSpy)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
s := (Storage)(nil)
m := &mock.Mock{}
// // Create: it should use the Legacy Create implementation
// _, err := dw.Create(context.Background(), &dummyObject{}, func(context.Context, runtime.Object) error { return nil }, &metav1.CreateOptions{})
// assert.NoError(t, err)
// assert.Equal(t, 0, lsSpy.Counts("LegacyStorage.Create"))
// assert.Equal(t, 1, sSpy.Counts("Storage.Create"))
ls := legacyStoreMock{m, l}
us := storageMock{m, s}
// // Get: it should use the Storage Get implementation
// _, err = dw.Get(context.Background(), kind, &metav1.GetOptions{})
// assert.NoError(t, err)
// assert.Equal(t, 0, lsSpy.Counts("LegacyStorage.Get"))
// assert.Equal(t, 1, sSpy.Counts("Storage.Get"))
if tt.setupStorageFn != nil {
tt.setupStorageFn(m, tt.input)
}
// // List: it should use the Storage Get implementation
// _, err = dw.List(context.Background(), &metainternalversion.ListOptions{})
// assert.NoError(t, err)
// assert.Equal(t, 0, lsSpy.Counts("LegacyStorage.List"))
// assert.Equal(t, 1, sSpy.Counts("Storage.List"))
dw := NewDualWriter(Mode4, ls, us, p)
// // Delete: it should use call Storage Delete method
// var deleteValidation = func(ctx context.Context, obj runtime.Object) error { return nil }
// _, _, err = dw.Delete(context.Background(), kind, deleteValidation, &metav1.DeleteOptions{})
// assert.NoError(t, err)
// assert.Equal(t, 0, lsSpy.Counts("LegacyStorage.Delete"))
// assert.Equal(t, 1, sSpy.Counts("Storage.Delete"))
obj, err := dw.Create(context.Background(), tt.input, func(context.Context, runtime.Object) error { return nil }, &metav1.CreateOptions{})
// // DeleteCollection: it should use the Storage DeleteCollection implementation
// _, err = dw.DeleteCollection(
// context.Background(),
// func(context.Context, runtime.Object) error { return nil },
// &metav1.DeleteOptions{},
// &metainternalversion.ListOptions{},
// )
// assert.NoError(t, err)
// assert.Equal(t, 0, lsSpy.Counts("LegacyStorage.DeleteCollection"))
// assert.Equal(t, 1, sSpy.Counts("Storage.DeleteCollection"))
if tt.wantErr {
assert.Error(t, err)
return
}
// // Update: it should update only in Storage
// dummy := &example.Pod{}
// uoi := UpdatedObjInfoObj{}
// _, err = uoi.UpdatedObject(context.Background(), dummy)
// assert.NoError(t, err)
acc, err := meta.Accessor(obj)
assert.NoError(t, err)
assert.Equal(t, acc.GetResourceVersion(), "1")
assert.NotEqual(t, obj, anotherObj)
})
}
}
// var validateObjFn = func(ctx context.Context, obj runtime.Object) error { return nil }
// var validateObjUpdateFn = func(ctx context.Context, obj, old runtime.Object) error { return nil }
func TestMode4_Get(t *testing.T) {
type testCase struct {
setupStorageFn func(m *mock.Mock, name string)
name string
input string
wantErr bool
}
tests :=
[]testCase{
{
name: "get an object only in unified store",
input: "foo",
setupStorageFn: func(m *mock.Mock, name string) {
m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil)
},
},
{
name: "error when getting an object in the inified store fails",
input: "object-fail",
setupStorageFn: func(m *mock.Mock, name string) {
m.On("Get", mock.Anything, name, mock.Anything).Return(nil, errors.New("error"))
},
wantErr: true,
},
}
// _, _, err = dw.Update(context.Background(), kind, uoi, validateObjFn, validateObjUpdateFn, false, &metav1.UpdateOptions{})
// assert.NoError(t, err)
// assert.Equal(t, 0, lsSpy.Counts("LegacyStorage.Update"))
// assert.Equal(t, 1, sSpy.Counts("Storage.Update"))
// assert.NoError(t, err)
// }
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
s := (Storage)(nil)
m := &mock.Mock{}
ls := legacyStoreMock{m, l}
us := storageMock{m, s}
if tt.setupStorageFn != nil {
tt.setupStorageFn(m, tt.input)
}
p := prometheus.NewRegistry()
dw := NewDualWriter(Mode4, ls, us, p)
obj, err := dw.Get(context.Background(), tt.input, &metav1.GetOptions{})
if tt.wantErr {
assert.Error(t, err)
return
}
us.AssertNotCalled(t, "Get", context.Background(), tt.name, &metav1.GetOptions{})
assert.Equal(t, obj, exampleObj)
assert.NotEqual(t, obj, anotherObj)
})
}
}
func TestMode4_List(t *testing.T) {
type testCase struct {
setupStorageFn func(m *mock.Mock, options *metainternalversion.ListOptions)
name string
options *metainternalversion.ListOptions
wantErr bool
}
tests :=
[]testCase{
{
name: "error when listing an object in the unified store is not implemented",
options: &metainternalversion.ListOptions{TypeMeta: metav1.TypeMeta{Kind: "fail"}},
setupStorageFn: func(m *mock.Mock, options *metainternalversion.ListOptions) {
m.On("List", mock.Anything, options).Return(nil, errors.New("error"))
},
wantErr: true,
},
{
name: "list objects in the unified store",
options: &metainternalversion.ListOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}},
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 := (LegacyStorage)(nil)
s := (Storage)(nil)
m := &mock.Mock{}
ls := legacyStoreMock{m, l}
us := storageMock{m, s}
if tt.setupStorageFn != nil {
tt.setupStorageFn(m, tt.options)
}
dw := NewDualWriter(Mode4, ls, us, p)
res, err := dw.List(context.Background(), tt.options)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.Equal(t, exampleList, res)
assert.NotEqual(t, anotherList, res)
})
}
}
func TestMode4_Delete(t *testing.T) {
type testCase struct {
setupStorageFn func(m *mock.Mock, name string)
name string
input string
wantErr bool
}
tests :=
[]testCase{
{
name: "deleting an object in the unified store",
input: "foo",
setupStorageFn: func(m *mock.Mock, name string) {
m.On("Delete", mock.Anything, name, mock.Anything, mock.Anything).Return(exampleObj, false, nil)
},
},
{
name: "error when deleting an object in the unified store",
input: "object-fail",
setupStorageFn: func(m *mock.Mock, name string) {
m.On("Delete", mock.Anything, name, mock.Anything, mock.Anything).Return(nil, false, errors.New("error"))
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
s := (Storage)(nil)
m := &mock.Mock{}
ls := legacyStoreMock{m, l}
us := storageMock{m, s}
if tt.setupStorageFn != nil {
tt.setupStorageFn(m, tt.input)
}
dw := NewDualWriter(Mode4, ls, us, p)
obj, _, err := dw.Delete(context.Background(), tt.input, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{})
if tt.wantErr {
assert.Error(t, err)
return
}
us.AssertNotCalled(t, "Delete", context.Background(), tt.input, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{})
assert.Equal(t, obj, exampleObj)
assert.NotEqual(t, obj, anotherObj)
})
}
}
func TestMode4_DeleteCollection(t *testing.T) {
type testCase struct {
input *metav1.DeleteOptions
setupStorageFn func(m *mock.Mock, input *metav1.DeleteOptions)
name string
wantErr bool
}
tests :=
[]testCase{
{
name: "deleting a collection in the unified store",
input: &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}},
setupStorageFn: func(m *mock.Mock, input *metav1.DeleteOptions) {
m.On("DeleteCollection", mock.Anything, mock.Anything, input, mock.Anything).Return(exampleObj, nil)
},
},
{
name: "error deleting a collection in the unified store",
input: &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: "fail"}},
setupStorageFn: func(m *mock.Mock, input *metav1.DeleteOptions) {
m.On("DeleteCollection", mock.Anything, mock.Anything, input, mock.Anything).Return(nil, errors.New("error"))
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
s := (Storage)(nil)
m := &mock.Mock{}
ls := legacyStoreMock{m, l}
us := storageMock{m, s}
if tt.setupStorageFn != nil {
tt.setupStorageFn(m, tt.input)
}
dw := NewDualWriter(Mode4, ls, us, p)
obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, tt.input, &metainternalversion.ListOptions{})
if tt.wantErr {
assert.Error(t, err)
return
}
us.AssertNotCalled(t, "DeleteCollection", context.Background(), tt.input, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{})
assert.Equal(t, obj, exampleObj)
assert.NotEqual(t, obj, anotherObj)
})
}
}
func TestMode4_Update(t *testing.T) {
type testCase struct {
setupStorageFn func(m *mock.Mock, input string)
name string
input string
wantErr bool
}
tests :=
[]testCase{
{
name: "update an object in unified store",
input: "foo",
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)
},
},
{
name: "error updating an object in unified store",
input: "object-fail",
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"))
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
s := (Storage)(nil)
m := &mock.Mock{}
ls := legacyStoreMock{m, l}
us := storageMock{m, s}
if tt.setupStorageFn != nil {
tt.setupStorageFn(m, tt.input)
}
dw := NewDualWriter(Mode4, ls, us, p)
obj, _, err := dw.Update(context.Background(), tt.input, 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 {
assert.Error(t, err)
return
}
assert.Equal(t, obj, exampleObj)
assert.NotEqual(t, obj, anotherObj)
})
}
}

View File

@ -174,12 +174,8 @@ func InstallAPIs(
if err != nil {
return nil, err
}
switch currentMode {
case grafanarest.Mode0:
if currentMode == grafanarest.Mode0 {
return legacy, nil
case grafanarest.Mode4:
return storage, nil
default:
}
return grafanarest.NewDualWriter(currentMode, legacy, storage, reg), nil
}