Storage: Add command line tool to migrate legacy dashboards (and folders) to unified storage (#99199)
This commit is contained in:
@@ -59,6 +59,7 @@ func NewBackend(opts BackendOptions) (Backend, error) {
|
||||
tracer: opts.Tracer,
|
||||
dbProvider: opts.DBProvider,
|
||||
pollingInterval: pollingInterval,
|
||||
batchLock: &batchLock{running: make(map[string]bool)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -77,6 +78,7 @@ type backend struct {
|
||||
dbProvider db.DBProvider
|
||||
db db.DB
|
||||
dialect sqltemplate.Dialect
|
||||
batchLock *batchLock
|
||||
|
||||
// watch streaming
|
||||
//stream chan *resource.WatchEvent
|
||||
@@ -701,7 +703,7 @@ func (b *backend) WatchWriteEvents(ctx context.Context) (<-chan *resource.Writte
|
||||
// Get the latest RV
|
||||
since, err := b.listLatestRVs(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get the latest resource version: %w", err)
|
||||
return nil, fmt.Errorf("watch, get latest resource version: %w", err)
|
||||
}
|
||||
// Start the poller
|
||||
stream := make(chan *resource.WrittenEvent)
|
||||
@@ -713,17 +715,23 @@ func (b *backend) poller(ctx context.Context, since groupResourceRV, stream chan
|
||||
t := time.NewTicker(b.pollingInterval)
|
||||
defer close(stream)
|
||||
defer t.Stop()
|
||||
isSQLite := b.dialect.DialectName() == "sqlite"
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.done:
|
||||
return
|
||||
case <-t.C:
|
||||
// Block polling duffing import to avoid database locked issues
|
||||
if isSQLite && b.batchLock.Active() {
|
||||
continue
|
||||
}
|
||||
|
||||
ctx, span := b.tracer.Start(ctx, tracePrefix+"poller")
|
||||
// List the latest RVs
|
||||
grv, err := b.listLatestRVs(ctx)
|
||||
if err != nil {
|
||||
b.log.Error("get the latest resource version", "err", err)
|
||||
b.log.Error("poller get latest resource version", "err", err)
|
||||
t.Reset(b.pollingInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/parquet"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"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/sqltemplate"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.BatchProcessingBackend = (*backend)(nil)
|
||||
)
|
||||
|
||||
type batchRV struct {
|
||||
max int64
|
||||
counter int64
|
||||
}
|
||||
|
||||
func newBatchRV() *batchRV {
|
||||
t := time.Now().Truncate(time.Second * 10)
|
||||
return &batchRV{
|
||||
max: (t.UnixMicro() / 10000000) * 10000000,
|
||||
counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (x *batchRV) next(obj metav1.Object) int64 {
|
||||
ts := obj.GetCreationTimestamp().UnixMicro()
|
||||
anno := obj.GetAnnotations()
|
||||
if anno != nil {
|
||||
v := anno[utils.AnnoKeyUpdatedTimestamp]
|
||||
t, err := time.Parse(time.RFC3339, v)
|
||||
if err == nil {
|
||||
ts = t.UnixMicro()
|
||||
}
|
||||
}
|
||||
if ts > x.max || ts < 10000000 {
|
||||
ts = x.max
|
||||
}
|
||||
x.counter++
|
||||
return (ts/10000000)*10000000 + x.counter
|
||||
}
|
||||
|
||||
type batchLock struct {
|
||||
running map[string]bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (x *batchLock) Start(keys []*resource.ResourceKey) error {
|
||||
x.mu.Lock()
|
||||
defer x.mu.Unlock()
|
||||
|
||||
// First verify that it is not already running
|
||||
ids := make([]string, len(keys))
|
||||
for i, k := range keys {
|
||||
id := k.BatchID()
|
||||
if x.running[id] {
|
||||
return &apierrors.StatusError{ErrStatus: metav1.Status{
|
||||
Code: http.StatusPreconditionFailed,
|
||||
Message: "batch export is already running",
|
||||
}}
|
||||
}
|
||||
ids[i] = id
|
||||
}
|
||||
|
||||
// Then add the keys to the lock
|
||||
for _, k := range ids {
|
||||
x.running[k] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *batchLock) Finish(keys []*resource.ResourceKey) {
|
||||
x.mu.Lock()
|
||||
defer x.mu.Unlock()
|
||||
for _, k := range keys {
|
||||
delete(x.running, k.BatchID())
|
||||
}
|
||||
}
|
||||
|
||||
func (x *batchLock) Active() bool {
|
||||
x.mu.Lock()
|
||||
defer x.mu.Unlock()
|
||||
return len(x.running) > 0
|
||||
}
|
||||
|
||||
func (b *backend) ProcessBatch(ctx context.Context, setting resource.BatchSettings, iter resource.BatchRequestIterator) *resource.BatchResponse {
|
||||
err := b.batchLock.Start(setting.Collection)
|
||||
if err != nil {
|
||||
return &resource.BatchResponse{
|
||||
Error: resource.AsErrorResult(err),
|
||||
}
|
||||
}
|
||||
defer b.batchLock.Finish(setting.Collection)
|
||||
|
||||
// We may want to first write parquet, then read parquet
|
||||
if b.dialect.DialectName() == "sqlite" {
|
||||
file, err := os.CreateTemp("", "grafana-batch-export-*.parquet")
|
||||
if err != nil {
|
||||
return &resource.BatchResponse{
|
||||
Error: resource.AsErrorResult(err),
|
||||
}
|
||||
}
|
||||
|
||||
writer, err := parquet.NewParquetWriter(file)
|
||||
if err != nil {
|
||||
return &resource.BatchResponse{
|
||||
Error: resource.AsErrorResult(err),
|
||||
}
|
||||
}
|
||||
|
||||
// write batch to parquet
|
||||
rsp := writer.ProcessBatch(ctx, setting, iter)
|
||||
if rsp.Error != nil {
|
||||
return rsp
|
||||
}
|
||||
|
||||
b.log.Info("using parquet buffer", "parquet", file)
|
||||
|
||||
// Replace the iterator with one from parquet
|
||||
iter, err = parquet.NewParquetReader(file.Name(), 50)
|
||||
if err != nil {
|
||||
return &resource.BatchResponse{
|
||||
Error: resource.AsErrorResult(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return b.processBatch(ctx, setting, iter)
|
||||
}
|
||||
|
||||
// internal batch process
|
||||
func (b *backend) processBatch(ctx context.Context, setting resource.BatchSettings, iter resource.BatchRequestIterator) *resource.BatchResponse {
|
||||
rsp := &resource.BatchResponse{}
|
||||
err := b.db.WithTx(ctx, ReadCommitted, func(ctx context.Context, tx db.Tx) error {
|
||||
rollbackWithError := func(err error) error {
|
||||
txerr := tx.Rollback()
|
||||
if txerr != nil {
|
||||
b.log.Warn("rollback", "error", txerr)
|
||||
} else {
|
||||
b.log.Info("rollback")
|
||||
}
|
||||
return err
|
||||
}
|
||||
batch := &batchWroker{
|
||||
ctx: ctx,
|
||||
tx: tx,
|
||||
dialect: b.dialect,
|
||||
logger: logging.FromContext(ctx),
|
||||
}
|
||||
|
||||
// Calculate the RV based on incoming request timestamps
|
||||
rv := newBatchRV()
|
||||
|
||||
summaries := make(map[string]*resource.BatchResponse_Summary, len(setting.Collection)*4)
|
||||
|
||||
// First clear everything in the transaction
|
||||
if setting.RebuildCollection {
|
||||
for _, key := range setting.Collection {
|
||||
summary, err := batch.deleteCollection(key)
|
||||
if err != nil {
|
||||
return rollbackWithError(err)
|
||||
}
|
||||
summaries[key.BatchID()] = summary
|
||||
rsp.Summary = append(rsp.Summary, summary)
|
||||
}
|
||||
}
|
||||
|
||||
obj := &unstructured.Unstructured{}
|
||||
|
||||
// Write each event into the history
|
||||
for iter.Next() {
|
||||
if iter.RollbackRequested() {
|
||||
return rollbackWithError(nil)
|
||||
}
|
||||
req := iter.Request()
|
||||
if req == nil {
|
||||
return rollbackWithError(fmt.Errorf("missing request"))
|
||||
}
|
||||
rsp.Processed++
|
||||
|
||||
if req.Action == resource.BatchRequest_UNKNOWN {
|
||||
rsp.Rejected = append(rsp.Rejected, &resource.BatchResponse_Rejected{
|
||||
Key: req.Key,
|
||||
Action: req.Action,
|
||||
Error: "unknown action",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
err := obj.UnmarshalJSON(req.Value)
|
||||
if err != nil {
|
||||
rsp.Rejected = append(rsp.Rejected, &resource.BatchResponse_Rejected{
|
||||
Key: req.Key,
|
||||
Action: req.Action,
|
||||
Error: "unable to unmarshal json",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Write the event to history
|
||||
if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryInsert, sqlResourceRequest{
|
||||
SQLTemplate: sqltemplate.New(b.dialect),
|
||||
WriteEvent: resource.WriteEvent{
|
||||
Key: req.Key,
|
||||
Type: resource.WatchEvent_Type(req.Action),
|
||||
Value: req.Value,
|
||||
PreviousRV: -1, // Used for WATCH, but we want to skip watch events
|
||||
},
|
||||
Folder: req.Folder,
|
||||
GUID: uuid.NewString(),
|
||||
ResourceVersion: rv.next(obj),
|
||||
}); err != nil {
|
||||
return rollbackWithError(fmt.Errorf("insert into resource history: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Now update the resource table from history
|
||||
for _, key := range setting.Collection {
|
||||
k := fmt.Sprintf("%s/%s/%s", key.Namespace, key.Group, key.Resource)
|
||||
summary := summaries[k]
|
||||
if summary == nil {
|
||||
return rollbackWithError(fmt.Errorf("missing summary key for: %s", k))
|
||||
}
|
||||
|
||||
err := batch.syncCollection(key, summary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure the collection RV is above our last written event
|
||||
_, err = b.resourceVersionAtomicInc(ctx, tx, key)
|
||||
if err != nil {
|
||||
b.log.Warn("error increasing RV", "error", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
rsp.Error = resource.AsErrorResult(err)
|
||||
}
|
||||
return rsp
|
||||
}
|
||||
|
||||
type batchWroker struct {
|
||||
ctx context.Context
|
||||
tx db.ContextExecer
|
||||
dialect sqltemplate.Dialect
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
// This will remove everything from the `resource` and `resource_history` table for a given namespace/group/resource
|
||||
func (w *batchWroker) deleteCollection(key *resource.ResourceKey) (*resource.BatchResponse_Summary, error) {
|
||||
summary := &resource.BatchResponse_Summary{
|
||||
Namespace: key.Namespace,
|
||||
Group: key.Group,
|
||||
Resource: key.Resource,
|
||||
}
|
||||
|
||||
// First delete history
|
||||
res, err := dbutil.Exec(w.ctx, w.tx, sqlResourceHistoryDelete, &sqlResourceHistoryDeleteRequest{
|
||||
SQLTemplate: sqltemplate.New(w.dialect),
|
||||
Namespace: key.Namespace,
|
||||
Group: key.Group,
|
||||
Resource: key.Resource,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
summary.PreviousHistory, err = res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Next delete the active resource table
|
||||
res, err = dbutil.Exec(w.ctx, w.tx, sqlResourceDelete, &sqlResourceRequest{
|
||||
SQLTemplate: sqltemplate.New(w.dialect),
|
||||
WriteEvent: resource.WriteEvent{
|
||||
Key: key,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
summary.PreviousCount, err = res.RowsAffected()
|
||||
return summary, err
|
||||
}
|
||||
|
||||
// Copy the latest value from history into the active resource table
|
||||
func (w *batchWroker) syncCollection(key *resource.ResourceKey, summary *resource.BatchResponse_Summary) error {
|
||||
w.logger.Info("synchronize collection", "key", key.BatchID())
|
||||
_, err := dbutil.Exec(w.ctx, w.tx, sqlResourceInsertFromHistory, &sqlResourceInsertFromHistoryRequest{
|
||||
SQLTemplate: sqltemplate.New(w.dialect),
|
||||
Key: key,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.logger.Info("get stats (still in transaction)", "key", key.BatchID())
|
||||
rows, err := dbutil.QueryRows(w.ctx, w.tx, sqlResourceStats, &sqlStatsRequest{
|
||||
SQLTemplate: sqltemplate.New(w.dialect),
|
||||
Namespace: key.Namespace,
|
||||
Group: key.Group,
|
||||
Resource: key.Resource,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows != nil {
|
||||
defer func() {
|
||||
_ = rows.Close()
|
||||
}()
|
||||
}
|
||||
if rows.Next() {
|
||||
row := resource.ResourceStats{}
|
||||
return rows.Scan(&row.Namespace, &row.Group, &row.Resource,
|
||||
&summary.Count,
|
||||
&summary.ResourceVersion)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func TestBatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("rv iterator", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rv := newBatchRV()
|
||||
v0 := rv.next(&unstructured.Unstructured{})
|
||||
v1 := rv.next(&unstructured.Unstructured{})
|
||||
v2 := rv.next(&unstructured.Unstructured{})
|
||||
require.True(t, v0 > 1000)
|
||||
require.Equal(t, int64(1), v1-v0)
|
||||
require.Equal(t, int64(1), v2-v1)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
INSERT INTO {{ .Ident "resource" }}
|
||||
SELECT
|
||||
kv.{{ .Ident "guid" }},
|
||||
kv.{{ .Ident "resource_version" }},
|
||||
kv.{{ .Ident "group" }},
|
||||
kv.{{ .Ident "resource" }},
|
||||
kv.{{ .Ident "namespace" }},
|
||||
kv.{{ .Ident "name" }},
|
||||
kv.{{ .Ident "value" }},
|
||||
kv.{{ .Ident "action" }},
|
||||
kv.{{ .Ident "label_set" }},
|
||||
kv.{{ .Ident "previous_resource_version" }},
|
||||
kv.{{ .Ident "folder" }}
|
||||
FROM {{ .Ident "resource_history" }} AS kv
|
||||
INNER JOIN (
|
||||
SELECT {{ .Ident "namespace" }}, {{ .Ident "group" }}, {{ .Ident "resource" }}, {{ .Ident "name" }}, max({{ .Ident "resource_version" }}) AS {{ .Ident "resource_version" }}
|
||||
FROM {{ .Ident "resource_history" }} AS mkv
|
||||
WHERE 1 = 1
|
||||
{{ if .Key.Namespace }}
|
||||
AND {{ .Ident "namespace" }} = {{ .Arg .Key.Namespace }}
|
||||
{{ end }}
|
||||
{{ if .Key.Group }}
|
||||
AND {{ .Ident "group" }} = {{ .Arg .Key.Group }}
|
||||
{{ end }}
|
||||
{{ if .Key.Resource }}
|
||||
AND {{ .Ident "resource" }} = {{ .Arg .Key.Resource }}
|
||||
{{ end }}
|
||||
{{ if .Key.Name }}
|
||||
AND {{ .Ident "name" }} = {{ .Arg .Key.Name }}
|
||||
{{ end }}
|
||||
GROUP BY mkv.{{ .Ident "namespace" }}, mkv.{{ .Ident "group" }}, mkv.{{ .Ident "resource" }}, mkv.{{ .Ident "name" }}
|
||||
) AS maxkv
|
||||
ON maxkv.{{ .Ident "resource_version" }} = kv.{{ .Ident "resource_version" }}
|
||||
AND maxkv.{{ .Ident "namespace" }} = kv.{{ .Ident "namespace" }}
|
||||
AND maxkv.{{ .Ident "group" }} = kv.{{ .Ident "group" }}
|
||||
AND maxkv.{{ .Ident "resource" }} = kv.{{ .Ident "resource" }}
|
||||
AND maxkv.{{ .Ident "name" }} = kv.{{ .Ident "name" }}
|
||||
WHERE kv.{{ .Ident "action" }} != 3
|
||||
{{ if .Key.Namespace }}
|
||||
AND kv.{{ .Ident "namespace" }} = {{ .Arg .Key.Namespace }}
|
||||
{{ end }}
|
||||
{{ if .Key.Group }}
|
||||
AND kv.{{ .Ident "group" }} = {{ .Arg .Key.Group }}
|
||||
{{ end }}
|
||||
{{ if .Key.Resource }}
|
||||
AND kv.{{ .Ident "resource" }} = {{ .Arg .Key.Resource }}
|
||||
{{ end }}
|
||||
{{ if .Key.Name }}
|
||||
AND kv.{{ .Ident "name" }} = {{ .Arg .Key.Name }}
|
||||
{{ end }}
|
||||
ORDER BY kv.{{ .Ident "resource_version" }} ASC
|
||||
;
|
||||
@@ -44,6 +44,7 @@ var (
|
||||
sqlResourceHistoryPoll = mustTemplate("resource_history_poll.sql")
|
||||
sqlResourceHistoryGet = mustTemplate("resource_history_get.sql")
|
||||
sqlResourceHistoryDelete = mustTemplate("resource_history_delete.sql")
|
||||
sqlResourceInsertFromHistory = mustTemplate("resource_insert_from_history.sql")
|
||||
|
||||
// sqlResourceLabelsInsert = mustTemplate("resource_labels_insert.sql")
|
||||
sqlResourceVersionGet = mustTemplate("resource_version_get.sql")
|
||||
@@ -83,6 +84,18 @@ func (r sqlResourceRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
type sqlResourceInsertFromHistoryRequest struct {
|
||||
sqltemplate.SQLTemplate
|
||||
Key *resource.ResourceKey
|
||||
}
|
||||
|
||||
func (r sqlResourceInsertFromHistoryRequest) Validate() error {
|
||||
if r.Key == nil {
|
||||
return fmt.Errorf("missing key")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type sqlStatsRequest struct {
|
||||
sqltemplate.SQLTemplate
|
||||
Namespace string
|
||||
|
||||
@@ -385,5 +385,18 @@ func TestUnifiedStorageQueries(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
sqlResourceInsertFromHistory: {
|
||||
{
|
||||
Name: "update",
|
||||
Data: &sqlResourceInsertFromHistoryRequest{
|
||||
SQLTemplate: mocks.NewTestingSQLTemplate(),
|
||||
Key: &resource.ResourceKey{
|
||||
Namespace: "default",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}})
|
||||
}
|
||||
|
||||
@@ -126,7 +126,9 @@ func (s *service) start(ctx context.Context) error {
|
||||
|
||||
srv := s.handler.GetServer()
|
||||
resource.RegisterResourceStoreServer(srv, server)
|
||||
resource.RegisterBatchStoreServer(srv, server)
|
||||
resource.RegisterResourceIndexServer(srv, server)
|
||||
resource.RegisterRepositoryIndexServer(srv, server)
|
||||
resource.RegisterBlobStoreServer(srv, server)
|
||||
resource.RegisterDiagnosticsServer(srv, server)
|
||||
grpc_health_v1.RegisterHealthServer(srv, healthService)
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
INSERT INTO `resource`
|
||||
SELECT
|
||||
kv.`guid`,
|
||||
kv.`resource_version`,
|
||||
kv.`group`,
|
||||
kv.`resource`,
|
||||
kv.`namespace`,
|
||||
kv.`name`,
|
||||
kv.`value`,
|
||||
kv.`action`,
|
||||
kv.`label_set`,
|
||||
kv.`previous_resource_version`,
|
||||
kv.`folder`
|
||||
FROM `resource_history` AS kv
|
||||
INNER JOIN (
|
||||
SELECT `namespace`, `group`, `resource`, `name`, max(`resource_version`) AS `resource_version`
|
||||
FROM `resource_history` AS mkv
|
||||
WHERE 1 = 1
|
||||
AND `namespace` = 'default'
|
||||
AND `group` = 'dashboard.grafana.app'
|
||||
AND `resource` = 'dashboards'
|
||||
GROUP BY mkv.`namespace`, mkv.`group`, mkv.`resource`, mkv.`name`
|
||||
) AS maxkv
|
||||
ON maxkv.`resource_version` = kv.`resource_version`
|
||||
AND maxkv.`namespace` = kv.`namespace`
|
||||
AND maxkv.`group` = kv.`group`
|
||||
AND maxkv.`resource` = kv.`resource`
|
||||
AND maxkv.`name` = kv.`name`
|
||||
WHERE kv.`action` != 3
|
||||
AND kv.`namespace` = 'default'
|
||||
AND kv.`group` = 'dashboard.grafana.app'
|
||||
AND kv.`resource` = 'dashboards'
|
||||
ORDER BY kv.`resource_version` ASC
|
||||
;
|
||||
Vendored
Executable
+34
@@ -0,0 +1,34 @@
|
||||
INSERT INTO "resource"
|
||||
SELECT
|
||||
kv."guid",
|
||||
kv."resource_version",
|
||||
kv."group",
|
||||
kv."resource",
|
||||
kv."namespace",
|
||||
kv."name",
|
||||
kv."value",
|
||||
kv."action",
|
||||
kv."label_set",
|
||||
kv."previous_resource_version",
|
||||
kv."folder"
|
||||
FROM "resource_history" AS kv
|
||||
INNER JOIN (
|
||||
SELECT "namespace", "group", "resource", "name", max("resource_version") AS "resource_version"
|
||||
FROM "resource_history" AS mkv
|
||||
WHERE 1 = 1
|
||||
AND "namespace" = 'default'
|
||||
AND "group" = 'dashboard.grafana.app'
|
||||
AND "resource" = 'dashboards'
|
||||
GROUP BY mkv."namespace", mkv."group", mkv."resource", mkv."name"
|
||||
) AS maxkv
|
||||
ON maxkv."resource_version" = kv."resource_version"
|
||||
AND maxkv."namespace" = kv."namespace"
|
||||
AND maxkv."group" = kv."group"
|
||||
AND maxkv."resource" = kv."resource"
|
||||
AND maxkv."name" = kv."name"
|
||||
WHERE kv."action" != 3
|
||||
AND kv."namespace" = 'default'
|
||||
AND kv."group" = 'dashboard.grafana.app'
|
||||
AND kv."resource" = 'dashboards'
|
||||
ORDER BY kv."resource_version" ASC
|
||||
;
|
||||
Vendored
Executable
+34
@@ -0,0 +1,34 @@
|
||||
INSERT INTO "resource"
|
||||
SELECT
|
||||
kv."guid",
|
||||
kv."resource_version",
|
||||
kv."group",
|
||||
kv."resource",
|
||||
kv."namespace",
|
||||
kv."name",
|
||||
kv."value",
|
||||
kv."action",
|
||||
kv."label_set",
|
||||
kv."previous_resource_version",
|
||||
kv."folder"
|
||||
FROM "resource_history" AS kv
|
||||
INNER JOIN (
|
||||
SELECT "namespace", "group", "resource", "name", max("resource_version") AS "resource_version"
|
||||
FROM "resource_history" AS mkv
|
||||
WHERE 1 = 1
|
||||
AND "namespace" = 'default'
|
||||
AND "group" = 'dashboard.grafana.app'
|
||||
AND "resource" = 'dashboards'
|
||||
GROUP BY mkv."namespace", mkv."group", mkv."resource", mkv."name"
|
||||
) AS maxkv
|
||||
ON maxkv."resource_version" = kv."resource_version"
|
||||
AND maxkv."namespace" = kv."namespace"
|
||||
AND maxkv."group" = kv."group"
|
||||
AND maxkv."resource" = kv."resource"
|
||||
AND maxkv."name" = kv."name"
|
||||
WHERE kv."action" != 3
|
||||
AND kv."namespace" = 'default'
|
||||
AND kv."group" = 'dashboard.grafana.app'
|
||||
AND kv."resource" = 'dashboards'
|
||||
ORDER BY kv."resource_version" ASC
|
||||
;
|
||||
Reference in New Issue
Block a user