|
|
|
|
@ -3,10 +3,13 @@ package rest |
|
|
|
|
import ( |
|
|
|
|
"context" |
|
|
|
|
|
|
|
|
|
"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" |
|
|
|
|
"k8s.io/apiserver/pkg/registry/rest" |
|
|
|
|
|
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
var ( |
|
|
|
|
@ -26,6 +29,7 @@ type Storage interface { |
|
|
|
|
rest.Scoper |
|
|
|
|
rest.TableConvertor |
|
|
|
|
rest.SingularNameProvider |
|
|
|
|
rest.Getter |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// LegacyStorage is a storage implementation that writes to the Grafana SQL database.
|
|
|
|
|
@ -38,7 +42,12 @@ type LegacyStorage interface { |
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
// 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
|
|
|
|
|
@ -54,6 +63,7 @@ type LegacyStorage interface { |
|
|
|
|
type DualWriter struct { |
|
|
|
|
Storage |
|
|
|
|
legacy LegacyStorage |
|
|
|
|
log log.Logger |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// NewDualWriter returns a new DualWriter.
|
|
|
|
|
@ -61,16 +71,23 @@ func NewDualWriter(legacy LegacyStorage, storage Storage) *DualWriter { |
|
|
|
|
return &DualWriter{ |
|
|
|
|
Storage: storage, |
|
|
|
|
legacy: legacy, |
|
|
|
|
log: log.New("grafana-apiserver.dualwriter"), |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Create overrides the default behavior of the Storage and writes to both the LegacyStorage and Storage.
|
|
|
|
|
func (d *DualWriter) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { |
|
|
|
|
if legacy, ok := d.legacy.(rest.Creater); ok { |
|
|
|
|
_, err := legacy.Create(ctx, obj, createValidation, options) |
|
|
|
|
created, err := legacy.Create(ctx, obj, createValidation, options) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
obj = created // write the updated version
|
|
|
|
|
rsp, err := d.Storage.Create(ctx, obj, createValidation, options) |
|
|
|
|
if err != nil { |
|
|
|
|
d.log.Error("unable to create object in duplicate storage", "error", err) |
|
|
|
|
} |
|
|
|
|
return rsp, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return d.Storage.Create(ctx, obj, createValidation, options) |
|
|
|
|
@ -79,10 +96,50 @@ func (d *DualWriter) Create(ctx context.Context, obj runtime.Object, createValid |
|
|
|
|
// Update overrides the default behavior of the Storage and writes to both the LegacyStorage and Storage.
|
|
|
|
|
func (d *DualWriter) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { |
|
|
|
|
if legacy, ok := d.legacy.(rest.Updater); ok { |
|
|
|
|
_, _, err := legacy.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) |
|
|
|
|
// Will resource version checking work????
|
|
|
|
|
old, err := d.Get(ctx, name, &metav1.GetOptions{}) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, false, err |
|
|
|
|
} |
|
|
|
|
accessor, err := meta.Accessor(old) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, false, err |
|
|
|
|
} |
|
|
|
|
// Hold on to the RV+UID for the dual write
|
|
|
|
|
theRV := accessor.GetResourceVersion() |
|
|
|
|
theUID := accessor.GetUID() |
|
|
|
|
|
|
|
|
|
// Changes applied within new storage
|
|
|
|
|
// will fail if RV is out of sync
|
|
|
|
|
updated, err := objInfo.UpdatedObject(ctx, old) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, false, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
accessor, err = meta.Accessor(updated) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, false, err |
|
|
|
|
} |
|
|
|
|
accessor.SetUID("") // clear it
|
|
|
|
|
accessor.SetResourceVersion("") // remove it so it is not a constraint
|
|
|
|
|
obj, created, err := legacy.Update(ctx, name, &updateWrapper{ |
|
|
|
|
upstream: objInfo, |
|
|
|
|
updated: updated, // returned as the object that will be updated
|
|
|
|
|
}, createValidation, updateValidation, forceAllowCreate, options) |
|
|
|
|
if err != nil { |
|
|
|
|
return obj, created, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
accessor, err = meta.Accessor(obj) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, false, err |
|
|
|
|
} |
|
|
|
|
accessor.SetResourceVersion(theRV) // the original RV
|
|
|
|
|
accessor.SetUID(theUID) |
|
|
|
|
objInfo = &updateWrapper{ |
|
|
|
|
upstream: objInfo, |
|
|
|
|
updated: obj, // returned as the object that will be updated
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) |
|
|
|
|
@ -90,24 +147,41 @@ func (d *DualWriter) Update(ctx context.Context, name string, objInfo rest.Updat |
|
|
|
|
|
|
|
|
|
// Delete overrides the default behavior of the Storage and delete from both the LegacyStorage and Storage.
|
|
|
|
|
func (d *DualWriter) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { |
|
|
|
|
if legacy, ok := d.legacy.(rest.GracefulDeleter); ok { |
|
|
|
|
_, _, err := legacy.Delete(ctx, name, deleteValidation, options) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, false, err |
|
|
|
|
// Delete from storage *first* so the item is still exists if a failure happens
|
|
|
|
|
obj, async, err := d.Storage.Delete(ctx, name, deleteValidation, options) |
|
|
|
|
if err == nil { |
|
|
|
|
if legacy, ok := d.legacy.(rest.GracefulDeleter); ok { |
|
|
|
|
obj, async, err = legacy.Delete(ctx, name, deleteValidation, options) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return d.Storage.Delete(ctx, name, deleteValidation, options) |
|
|
|
|
return obj, async, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// DeleteCollection overrides the default behavior of the Storage and delete from both the LegacyStorage and Storage.
|
|
|
|
|
func (d *DualWriter) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) { |
|
|
|
|
if legacy, ok := d.legacy.(rest.CollectionDeleter); ok { |
|
|
|
|
_, err := legacy.DeleteCollection(ctx, deleteValidation, options, listOptions) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
out, err := d.Storage.DeleteCollection(ctx, deleteValidation, options, listOptions) |
|
|
|
|
if err == nil { |
|
|
|
|
if legacy, ok := d.legacy.(rest.CollectionDeleter); ok { |
|
|
|
|
out, err = legacy.DeleteCollection(ctx, deleteValidation, options, listOptions) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return out, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return d.Storage.DeleteCollection(ctx, deleteValidation, options, listOptions) |
|
|
|
|
// 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 |
|
|
|
|
} |
|
|
|
|
|