grafana/pkg/apiserver/rest/dualwriter.go
Arati R 0ffc4c441b
Storage: Add mode reconciliation for modes 1 and 2 (#87919)
* Add skeleton implementation for mode reconciliation between 1 and 2
* Track mode for each dual writer
* Add test for setting dual writer
* Include context when setting dual writing mode

---------

Co-authored-by: Dan Cech <dcech@grafana.com>
2024-05-23 00:12:46 +02:00

193 lines
5.8 KiB
Go

package rest
import (
"context"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/services/featuremgmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/klog/v2"
)
var (
_ rest.Storage = (DualWriter)(nil)
_ rest.Scoper = (DualWriter)(nil)
_ rest.TableConvertor = (DualWriter)(nil)
_ rest.CreaterUpdater = (DualWriter)(nil)
_ rest.CollectionDeleter = (DualWriter)(nil)
_ rest.GracefulDeleter = (DualWriter)(nil)
_ rest.SingularNameProvider = (DualWriter)(nil)
)
// Storage is a storage implementation that satisfies the same interfaces as genericregistry.Store.
type Storage interface {
rest.Storage
rest.Scoper
rest.TableConvertor
rest.SingularNameProvider
rest.Getter
// TODO: when watch is implemented, we can replace all the below with rest.StandardStorage
rest.Lister
rest.CreaterUpdater
rest.GracefulDeleter
rest.CollectionDeleter
}
// LegacyStorage is a storage implementation that writes to the Grafana SQL database.
type LegacyStorage interface {
rest.Storage
rest.Scoper
rest.SingularNameProvider
rest.CreaterUpdater
rest.Lister
rest.GracefulDeleter
rest.CollectionDeleter
rest.TableConvertor
rest.Getter
}
// DualWriter is a storage implementation that writes first to LegacyStorage and then to Storage.
// If writing to LegacyStorage fails, the write to Storage is skipped and the error is returned.
// Storage is used for all read operations. This is useful as a migration step from SQL based
// legacy storage to a more standard kubernetes backed storage interface.
//
// NOTE: Only values supported by legacy storage will be preserved in the CREATE/UPDATE commands.
// For example, annotations, labels, and managed fields may not be preserved. Everything in upstream
// storage can be recrated from the data in legacy storage.
//
// The LegacyStorage implementation must implement the following interfaces:
// - rest.Storage
// - rest.TableConvertor
// - rest.Scoper
// - rest.SingularNameProvider
//
// These interfaces are optional, but they all should be implemented to fully support dual writes:
// - rest.Creater
// - rest.Updater
// - rest.GracefulDeleter
// - rest.CollectionDeleter
type DualWriter interface {
Storage
LegacyStorage
Mode() DualWriterMode
}
type DualWriterMode int
const (
Mode1 DualWriterMode = iota + 1
Mode2
Mode3
Mode4
)
// NewDualWriter returns a new DualWriter.
func NewDualWriter(mode DualWriterMode, legacy LegacyStorage, storage Storage) DualWriter {
switch mode {
case Mode1:
// read and write only from legacy storage
return NewDualWriterMode1(legacy, storage)
case Mode2:
// write to both, read from storage but use legacy as backup
return NewDualWriterMode2(legacy, storage)
case Mode3:
// write to both, read from storage only
return NewDualWriterMode3(legacy, storage)
case Mode4:
// read and write only from storage
return NewDualWriterMode4(legacy, storage)
default:
return NewDualWriterMode1(legacy, storage)
}
}
type updateWrapper struct {
upstream rest.UpdatedObjectInfo
updated runtime.Object
}
// Returns preconditions built from the updated object, if applicable.
// May return nil, or a preconditions object containing nil fields,
// if no preconditions can be determined from the updated object.
func (u *updateWrapper) Preconditions() *metav1.Preconditions {
return u.upstream.Preconditions()
}
// UpdatedObject returns the updated object, given a context and old object.
// The only time an empty oldObj should be passed in is if a "create on update" is occurring (there is no oldObj).
func (u *updateWrapper) UpdatedObject(ctx context.Context, oldObj runtime.Object) (newObj runtime.Object, err error) {
return u.updated, nil
}
func SetDualWritingMode(
ctx context.Context,
kvs *kvstore.NamespacedKVStore,
features featuremgmt.FeatureToggles,
entity string,
legacy LegacyStorage,
storage Storage,
) (DualWriter, error) {
toMode := map[string]DualWriterMode{
"1": Mode1,
"2": Mode2,
"3": Mode3,
"4": Mode4,
}
errDualWriterSetCurrentMode := errors.New("failed to set current dual writing mode")
// Use entity name as key
m, ok, err := kvs.Get(ctx, entity)
if err != nil {
return nil, errors.New("failed to fetch current dual writing mode")
}
currentMode, valid := toMode[m]
if !valid && ok {
// Only log if "ok" because initially all instances will have mode unset for playlists.
klog.Info("invalid dual writing mode for playlists mode:", m)
}
if !valid || !ok {
// Default to mode 1
currentMode = Mode1
err := kvs.Set(ctx, entity, fmt.Sprint(currentMode))
if err != nil {
return nil, errDualWriterSetCurrentMode
}
}
// Desired mode is 2 and current mode is 1
if features.IsEnabledGlobally(featuremgmt.FlagDualWritePlaylistsMode2) && (currentMode == Mode1) {
// This is where we go through the different gates to allow the instance to migrate from mode 1 to mode 2.
// There are none between mode 1 and mode 2
currentMode = Mode2
err := kvs.Set(ctx, entity, fmt.Sprint(currentMode))
if err != nil {
return nil, errDualWriterSetCurrentMode
}
}
// #TODO enable this check when we have a flag/config for setting mode 1 as the desired mode
// if features.IsEnabledGlobally(featuremgmt.FlagDualWritePlaylistsMode1) && (currentMode == Mode2) {
// // This is where we go through the different gates to allow the instance to migrate from mode 2 to mode 1.
// // There are none between mode 1 and mode 2
// currentMode = Mode1
// err := kvs.Set(ctx, entity, fmt.Sprint(currentMode))
// if err != nil {
// return nil, errDualWriterSetCurrentMode
// }
// }
// #TODO add support for other combinations of desired and current modes
return NewDualWriter(currentMode, legacy, storage), nil
}