0284d1e669
* unified-storage: add `UnixTimestamp` support to sqlkv implementation * unified-storage: improve tests and enable all of them on sqlkv
569 lines
14 KiB
Go
569 lines
14 KiB
Go
package resource
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"embed"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"iter"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/grafana/grafana/pkg/storage/unified/sql/db"
|
|
"github.com/grafana/grafana/pkg/storage/unified/sql/dbutil"
|
|
"github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager"
|
|
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
|
)
|
|
|
|
// Templates setup.
|
|
var (
|
|
//go:embed data/*.sql
|
|
sqlTemplatesFS embed.FS
|
|
|
|
sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `data/*.sql`))
|
|
)
|
|
|
|
func mustTemplate(filename string) *template.Template {
|
|
if t := sqlTemplates.Lookup(filename); t != nil {
|
|
return t
|
|
}
|
|
panic(fmt.Sprintf("template file not found: %s", filename))
|
|
}
|
|
|
|
// Templates.
|
|
var (
|
|
sqlKVKeys = mustTemplate("sqlkv_keys.sql")
|
|
sqlKVGet = mustTemplate("sqlkv_get.sql")
|
|
sqlKVBatchGet = mustTemplate("sqlkv_batch_get.sql")
|
|
sqlKVSaveEvent = mustTemplate("sqlkv_save_event.sql")
|
|
sqlKVInsertData = mustTemplate("sqlkv_insert_datastore.sql")
|
|
sqlKVUpdateData = mustTemplate("sqlkv_update_datastore.sql")
|
|
sqlKVInsertLegacyResourceHistory = mustTemplate("sqlkv_insert_legacy_resource_history.sql")
|
|
sqlKVInsertLegacyResource = mustTemplate("sqlkv_insert_legacy_resource.sql")
|
|
sqlKVUpdateLegacyResource = mustTemplate("sqlkv_update_legacy_resource.sql")
|
|
sqlKVDeleteLegacyResource = mustTemplate("sqlkv_delete_legacy_resource.sql")
|
|
sqlKVDelete = mustTemplate("sqlkv_delete.sql")
|
|
sqlKVBatchDelete = mustTemplate("sqlkv_batch_delete.sql")
|
|
)
|
|
|
|
// sqlKVSection can be embedded in structs used when rendering query templates
|
|
// for queries that reference a particular section. The section will be validated,
|
|
// and the template can directly reference the `TableName`.
|
|
type sqlKVSection struct {
|
|
Section string
|
|
}
|
|
|
|
func (req sqlKVSection) Validate() error {
|
|
if req.Section == "" {
|
|
return fmt.Errorf("section is required")
|
|
}
|
|
|
|
if req.Section != dataSection && req.Section != eventsSection {
|
|
return fmt.Errorf("invalid section: %s", req.Section)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (req sqlKVSection) TableName() string {
|
|
if req.Section == dataSection {
|
|
return "resource_history"
|
|
}
|
|
|
|
return "resource_events"
|
|
}
|
|
|
|
// sqlKVSectionKey can be embedded in structs used when rendering query templates
|
|
// for queries that reference both a section and a particular key. The `key` is
|
|
// validated, and the template can reference the corresponding `KeyPath`.
|
|
type sqlKVSectionKey struct {
|
|
sqlKVSection
|
|
Key string
|
|
}
|
|
|
|
func (req sqlKVSectionKey) Validate() error {
|
|
if err := req.sqlKVSection.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if req.Key == "" {
|
|
return fmt.Errorf("key is required")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (req sqlKVSectionKey) KeyPath() string {
|
|
return req.Section + "/" + req.Key
|
|
}
|
|
|
|
type sqlKVGetRequest struct {
|
|
sqltemplate.SQLTemplate
|
|
sqlKVSectionKey
|
|
*sqlKVGetResponse
|
|
}
|
|
|
|
type sqlKVGetResponse struct {
|
|
Value []byte
|
|
}
|
|
|
|
func (req sqlKVGetRequest) Validate() error {
|
|
return req.sqlKVSectionKey.Validate()
|
|
}
|
|
|
|
func (req sqlKVGetRequest) Results() ([]byte, error) {
|
|
return req.Value, nil
|
|
}
|
|
|
|
type sqlKVBatchRequest struct {
|
|
sqltemplate.SQLTemplate
|
|
sqlKVSection
|
|
Keys []string
|
|
}
|
|
|
|
func (req sqlKVBatchRequest) Validate() error {
|
|
return req.sqlKVSection.Validate()
|
|
}
|
|
|
|
func (req sqlKVBatchRequest) KeyPaths() []string {
|
|
result := make([]string, 0, len(req.Keys))
|
|
for _, key := range req.Keys {
|
|
result = append(result, req.Section+"/"+key)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
type sqlKVSaveRequest struct {
|
|
sqltemplate.SQLTemplate
|
|
sqlKVSectionKey
|
|
Value []byte
|
|
|
|
// old fields that can be removed once we prune resource_history
|
|
GUID string
|
|
Group string
|
|
Resource string
|
|
Namespace string
|
|
Name string
|
|
Action int64
|
|
Folder string
|
|
}
|
|
|
|
func (req sqlKVSaveRequest) Validate() error {
|
|
return req.sqlKVSectionKey.Validate()
|
|
}
|
|
|
|
type sqlKVLegacySaveRequest struct {
|
|
sqltemplate.SQLTemplate
|
|
Value []byte
|
|
GUID string
|
|
Group string
|
|
Resource string
|
|
Namespace string
|
|
Name string
|
|
Action int64
|
|
Folder string
|
|
}
|
|
|
|
func (req sqlKVLegacySaveRequest) Validate() error {
|
|
return nil
|
|
}
|
|
|
|
func (req sqlKVLegacySaveRequest) Results() ([]byte, error) {
|
|
return req.Value, nil
|
|
}
|
|
|
|
type sqlKVKeysRequest struct {
|
|
sqltemplate.SQLTemplate
|
|
sqlKVSection
|
|
Options ListOptions
|
|
}
|
|
|
|
func (req sqlKVKeysRequest) Validate() error {
|
|
return req.sqlKVSection.Validate()
|
|
}
|
|
|
|
func (req sqlKVKeysRequest) StartKey() string {
|
|
return req.Section + "/" + req.Options.StartKey
|
|
}
|
|
|
|
func (req sqlKVKeysRequest) EndKey() string {
|
|
if req.Options.EndKey == "" {
|
|
req.Options.EndKey = PrefixRangeEnd(req.Section + "/")
|
|
}
|
|
|
|
return req.Section + "/" + req.Options.EndKey
|
|
}
|
|
|
|
func (req sqlKVKeysRequest) SortAscending() bool {
|
|
return req.Options.Sort != SortOrderDesc
|
|
}
|
|
|
|
type sqlKVDeleteRequest struct {
|
|
sqltemplate.SQLTemplate
|
|
sqlKVSectionKey
|
|
}
|
|
|
|
func (req sqlKVDeleteRequest) Validate() error {
|
|
return req.sqlKVSectionKey.Validate()
|
|
}
|
|
|
|
var _ KV = &sqlKV{}
|
|
|
|
type sqlKV struct {
|
|
dbProvider db.DBProvider
|
|
db db.DB
|
|
dialect sqltemplate.Dialect
|
|
}
|
|
|
|
func NewSQLKV(dbProvider db.DBProvider) (KV, error) {
|
|
if dbProvider == nil {
|
|
return nil, fmt.Errorf("dbProvider is required")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
dbConn, err := dbProvider.Init(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error initializing DB: %w", err)
|
|
}
|
|
|
|
dialect := sqltemplate.DialectForDriver(dbConn.DriverName())
|
|
if dialect == nil {
|
|
return nil, fmt.Errorf("unsupported database driver: %s", dbConn.DriverName())
|
|
}
|
|
|
|
return &sqlKV{
|
|
dbProvider: dbProvider,
|
|
db: dbConn,
|
|
dialect: dialect,
|
|
}, nil
|
|
}
|
|
|
|
func (k *sqlKV) Ping(ctx context.Context) error {
|
|
return k.db.PingContext(ctx)
|
|
}
|
|
|
|
func (k *sqlKV) Keys(ctx context.Context, section string, opt ListOptions) iter.Seq2[string, error] {
|
|
return func(yield func(string, error) bool) {
|
|
rows, err := dbutil.QueryRows(ctx, k.db, sqlKVKeys, sqlKVKeysRequest{
|
|
SQLTemplate: sqltemplate.New(k.dialect),
|
|
sqlKVSection: sqlKVSection{section},
|
|
Options: opt,
|
|
})
|
|
if err != nil {
|
|
yield("", err)
|
|
return
|
|
}
|
|
defer closeRows(rows, yield)
|
|
|
|
for rows.Next() {
|
|
var key string
|
|
if err := rows.Scan(&key); err != nil {
|
|
yield("", fmt.Errorf("error reading row: %w", err))
|
|
return
|
|
}
|
|
|
|
if !yield(strings.TrimPrefix(key, section+"/"), nil) {
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
yield("", fmt.Errorf("failed to read rows: %w", err))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (k *sqlKV) Get(ctx context.Context, section string, key string) (io.ReadCloser, error) {
|
|
value, err := dbutil.QueryRow(ctx, k.db, sqlKVGet, sqlKVGetRequest{
|
|
SQLTemplate: sqltemplate.New(k.dialect),
|
|
sqlKVSectionKey: sqlKVSectionKey{sqlKVSection{section}, key},
|
|
sqlKVGetResponse: new(sqlKVGetResponse),
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to get key: %w", err)
|
|
}
|
|
|
|
return io.NopCloser(bytes.NewReader(value)), nil
|
|
}
|
|
|
|
func (k *sqlKV) BatchGet(ctx context.Context, section string, keys []string) iter.Seq2[KeyValue, error] {
|
|
return func(yield func(KeyValue, error) bool) {
|
|
if len(keys) == 0 {
|
|
return
|
|
}
|
|
|
|
rows, err := dbutil.QueryRows(ctx, k.db, sqlKVBatchGet, sqlKVBatchRequest{
|
|
SQLTemplate: sqltemplate.New(k.dialect),
|
|
sqlKVSection: sqlKVSection{section},
|
|
Keys: keys,
|
|
})
|
|
if err != nil {
|
|
yield(KeyValue{}, err)
|
|
return
|
|
}
|
|
defer closeRows(rows, yield)
|
|
|
|
for rows.Next() {
|
|
var key string
|
|
var value []byte
|
|
if err := rows.Scan(&key, &value); err != nil {
|
|
yield(KeyValue{}, fmt.Errorf("error reading row: %w", err))
|
|
return
|
|
}
|
|
|
|
kv := KeyValue{
|
|
Key: strings.TrimPrefix(key, section+"/"),
|
|
Value: io.NopCloser(bytes.NewReader(value)),
|
|
}
|
|
if !yield(kv, nil) {
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
yield(KeyValue{}, fmt.Errorf("failed to read rows: %w", err))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (k *sqlKV) Save(ctx context.Context, section string, key string) (io.WriteCloser, error) {
|
|
sectionKey := sqlKVSectionKey{sqlKVSection{section}, key}
|
|
if err := sectionKey.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &sqlWriteCloser{
|
|
kv: k,
|
|
ctx: ctx,
|
|
sectionKey: sectionKey,
|
|
buf: &bytes.Buffer{},
|
|
closed: false,
|
|
}, nil
|
|
}
|
|
|
|
type sqlWriteCloser struct {
|
|
kv *sqlKV
|
|
ctx context.Context
|
|
sectionKey sqlKVSectionKey
|
|
buf *bytes.Buffer
|
|
closed bool
|
|
}
|
|
|
|
func (w *sqlWriteCloser) Write(value []byte) (int, error) {
|
|
if w.closed {
|
|
return 0, errors.New("write to closed writer")
|
|
}
|
|
|
|
return w.buf.Write(value)
|
|
}
|
|
|
|
func (w *sqlWriteCloser) Close() error {
|
|
if w.closed {
|
|
return nil
|
|
}
|
|
|
|
w.closed = true
|
|
|
|
// do regular kv save: simple key_path + value insert with conflict check.
|
|
// can only do this on resource_events for now, until we drop the columns in resource_history
|
|
if w.sectionKey.Section == eventsSection {
|
|
_, err := dbutil.Exec(w.ctx, w.kv.db, sqlKVSaveEvent, sqlKVSaveRequest{
|
|
SQLTemplate: sqltemplate.New(w.kv.dialect),
|
|
sqlKVSectionKey: w.sectionKey,
|
|
Value: w.buf.Bytes(),
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// if storage_backend is running with an RvManager, it will inject a transaction into the context
|
|
// used to keep backwards compatibility between sql-based kvstore and unified/sql/backend
|
|
tx, ok := rvmanager.TxFromCtx(w.ctx)
|
|
if !ok {
|
|
// temporary save for dataStore without rvmanager
|
|
// we can use the same template as the event one after we:
|
|
// - move PK from GUID to key_path
|
|
// - remove all unnecessary columns (or at least their NOT NULL constraints)
|
|
_, err := w.kv.Get(w.ctx, w.sectionKey.Section, w.sectionKey.Key)
|
|
if errors.Is(err, ErrNotFound) {
|
|
_, err := dbutil.Exec(w.ctx, w.kv.db, sqlKVInsertData, sqlKVSaveRequest{
|
|
SQLTemplate: sqltemplate.New(w.kv.dialect),
|
|
sqlKVSectionKey: w.sectionKey,
|
|
GUID: uuid.New().String(),
|
|
Value: w.buf.Bytes(),
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to insert to datastore: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get for save: %w", err)
|
|
}
|
|
|
|
_, err = dbutil.Exec(w.ctx, w.kv.db, sqlKVUpdateData, sqlKVSaveRequest{
|
|
SQLTemplate: sqltemplate.New(w.kv.dialect),
|
|
sqlKVSectionKey: w.sectionKey,
|
|
Value: w.buf.Bytes(),
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update to datastore: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// special, temporary save that includes all the fields in resource_history that are not relevant for the kvstore,
|
|
// as well as the resource table. This is only called if an RvManager was passed to storage_backend, as that
|
|
// component will be responsible for populating the resource_version and key_path columns
|
|
// note that we are not touching resource_version table, neither the resource_version columns or the key_path column
|
|
// as the RvManager will be responsible for this
|
|
dataKey, err := ParseKeyWithGUID(w.sectionKey.Key)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse key: %w", err)
|
|
}
|
|
|
|
var action int64
|
|
switch dataKey.Action {
|
|
case DataActionCreated:
|
|
action = 1
|
|
case DataActionUpdated:
|
|
action = 2
|
|
case DataActionDeleted:
|
|
action = 3
|
|
default:
|
|
return fmt.Errorf("failed to parse key: %w", err)
|
|
}
|
|
|
|
_, err = dbutil.Exec(w.ctx, tx, sqlKVInsertLegacyResourceHistory, sqlKVSaveRequest{
|
|
SQLTemplate: sqltemplate.New(w.kv.dialect),
|
|
sqlKVSectionKey: w.sectionKey,
|
|
Value: w.buf.Bytes(),
|
|
GUID: dataKey.GUID,
|
|
Group: dataKey.Group,
|
|
Resource: dataKey.Resource,
|
|
Namespace: dataKey.Namespace,
|
|
Name: dataKey.Name,
|
|
Action: action,
|
|
Folder: dataKey.Folder,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save to resource_history: %w", err)
|
|
}
|
|
|
|
switch dataKey.Action {
|
|
case DataActionCreated:
|
|
_, err = dbutil.Exec(w.ctx, tx, sqlKVInsertLegacyResource, sqlKVLegacySaveRequest{
|
|
SQLTemplate: sqltemplate.New(w.kv.dialect),
|
|
Value: w.buf.Bytes(),
|
|
GUID: dataKey.GUID,
|
|
Group: dataKey.Group,
|
|
Resource: dataKey.Resource,
|
|
Namespace: dataKey.Namespace,
|
|
Name: dataKey.Name,
|
|
Action: action,
|
|
Folder: dataKey.Folder,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to insert to resource: %w", err)
|
|
}
|
|
case DataActionUpdated:
|
|
_, err = dbutil.Exec(w.ctx, tx, sqlKVUpdateLegacyResource, sqlKVLegacySaveRequest{
|
|
SQLTemplate: sqltemplate.New(w.kv.dialect),
|
|
Value: w.buf.Bytes(),
|
|
Group: dataKey.Group,
|
|
Resource: dataKey.Resource,
|
|
Namespace: dataKey.Namespace,
|
|
Name: dataKey.Name,
|
|
Action: action,
|
|
Folder: dataKey.Folder,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update resource: %w", err)
|
|
}
|
|
case DataActionDeleted:
|
|
_, err = dbutil.Exec(w.ctx, tx, sqlKVDeleteLegacyResource, sqlKVLegacySaveRequest{
|
|
SQLTemplate: sqltemplate.New(w.kv.dialect),
|
|
Group: dataKey.Group,
|
|
Resource: dataKey.Resource,
|
|
Namespace: dataKey.Namespace,
|
|
Name: dataKey.Name,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete from resource: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (k *sqlKV) Delete(ctx context.Context, section string, key string) error {
|
|
res, err := dbutil.Exec(ctx, k.db, sqlKVDelete, sqlKVDeleteRequest{
|
|
SQLTemplate: sqltemplate.New(k.dialect),
|
|
sqlKVSectionKey: sqlKVSectionKey{sqlKVSection{section}, key},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete key: %w", err)
|
|
}
|
|
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to validate delete: %w", err)
|
|
}
|
|
|
|
if rows == 0 {
|
|
return ErrNotFound
|
|
}
|
|
|
|
// TODO reflect change to resource table
|
|
|
|
return nil
|
|
}
|
|
|
|
func (k *sqlKV) BatchDelete(ctx context.Context, section string, keys []string) error {
|
|
if len(keys) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if _, err := dbutil.Exec(ctx, k.db, sqlKVBatchDelete, sqlKVBatchRequest{
|
|
SQLTemplate: sqltemplate.New(k.dialect),
|
|
sqlKVSection: sqlKVSection{section},
|
|
Keys: keys,
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to batch delete keys: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (k *sqlKV) UnixTimestamp(ctx context.Context) (int64, error) {
|
|
return time.Now().Unix(), nil
|
|
}
|
|
|
|
func closeRows[T any](rows db.Rows, yield func(T, error) bool) {
|
|
if err := rows.Close(); err != nil {
|
|
var zero T
|
|
yield(zero, fmt.Errorf("error closing rows: %w", err))
|
|
}
|
|
}
|