2022-10-19 19:05:51 -05:00
|
|
|
package export
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/grafana/grafana/pkg/infra/db"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
|
|
"github.com/grafana/grafana/pkg/models"
|
2022-11-01 10:28:13 -05:00
|
|
|
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
2022-10-19 19:05:51 -05:00
|
|
|
"github.com/grafana/grafana/pkg/services/playlist"
|
|
|
|
"github.com/grafana/grafana/pkg/services/sqlstore/session"
|
|
|
|
"github.com/grafana/grafana/pkg/services/store"
|
2022-11-01 10:28:13 -05:00
|
|
|
"github.com/grafana/grafana/pkg/services/store/kind/snapshot"
|
2022-10-19 19:05:51 -05:00
|
|
|
"github.com/grafana/grafana/pkg/services/store/object"
|
|
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
|
|
)
|
|
|
|
|
|
|
|
var _ Job = new(objectStoreJob)
|
|
|
|
|
|
|
|
type objectStoreJob struct {
|
|
|
|
logger log.Logger
|
|
|
|
|
|
|
|
statusMu sync.Mutex
|
|
|
|
status ExportStatus
|
|
|
|
cfg ExportConfig
|
|
|
|
broadcaster statusBroadcaster
|
|
|
|
stopRequested bool
|
2022-11-10 13:16:31 -06:00
|
|
|
ctx context.Context
|
2022-10-19 19:05:51 -05:00
|
|
|
|
2022-11-01 10:28:13 -05:00
|
|
|
sess *session.SessionDB
|
|
|
|
playlistService playlist.Service
|
|
|
|
store object.ObjectStoreServer
|
|
|
|
dashboardsnapshots dashboardsnapshots.Service
|
2022-10-19 19:05:51 -05:00
|
|
|
}
|
|
|
|
|
2022-11-10 13:16:31 -06:00
|
|
|
func startObjectStoreJob(ctx context.Context,
|
2022-11-01 10:28:13 -05:00
|
|
|
cfg ExportConfig,
|
|
|
|
broadcaster statusBroadcaster,
|
|
|
|
db db.DB,
|
|
|
|
playlistService playlist.Service,
|
|
|
|
store object.ObjectStoreServer,
|
|
|
|
dashboardsnapshots dashboardsnapshots.Service,
|
|
|
|
) (Job, error) {
|
2022-10-19 19:05:51 -05:00
|
|
|
job := &objectStoreJob{
|
|
|
|
logger: log.New("export_to_object_store_job"),
|
|
|
|
cfg: cfg,
|
2022-11-10 13:16:31 -06:00
|
|
|
ctx: ctx,
|
2022-10-19 19:05:51 -05:00
|
|
|
broadcaster: broadcaster,
|
|
|
|
status: ExportStatus{
|
|
|
|
Running: true,
|
|
|
|
Target: "object store export",
|
|
|
|
Started: time.Now().UnixMilli(),
|
|
|
|
Count: make(map[string]int, 10),
|
|
|
|
Index: 0,
|
|
|
|
},
|
2022-11-01 10:28:13 -05:00
|
|
|
sess: db.GetSqlxSession(),
|
|
|
|
playlistService: playlistService,
|
|
|
|
store: store,
|
|
|
|
dashboardsnapshots: dashboardsnapshots,
|
2022-10-19 19:05:51 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
broadcaster(job.status)
|
2022-11-10 13:16:31 -06:00
|
|
|
go job.start(ctx)
|
2022-10-19 19:05:51 -05:00
|
|
|
return job, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *objectStoreJob) requestStop() {
|
|
|
|
e.stopRequested = true
|
|
|
|
}
|
|
|
|
|
2022-11-10 13:16:31 -06:00
|
|
|
func (e *objectStoreJob) start(ctx context.Context) {
|
2022-10-19 19:05:51 -05:00
|
|
|
defer func() {
|
|
|
|
e.logger.Info("Finished dummy export job")
|
|
|
|
|
|
|
|
e.statusMu.Lock()
|
|
|
|
defer e.statusMu.Unlock()
|
|
|
|
s := e.status
|
|
|
|
if err := recover(); err != nil {
|
|
|
|
e.logger.Error("export panic", "error", err)
|
|
|
|
s.Status = fmt.Sprintf("ERROR: %v", err)
|
|
|
|
}
|
|
|
|
// Make sure it finishes OK
|
|
|
|
if s.Finished < 10 {
|
|
|
|
s.Finished = time.Now().UnixMilli()
|
|
|
|
}
|
|
|
|
s.Running = false
|
|
|
|
if s.Status == "" {
|
|
|
|
s.Status = "done"
|
|
|
|
}
|
|
|
|
e.status = s
|
|
|
|
e.broadcaster(s)
|
|
|
|
}()
|
|
|
|
|
|
|
|
e.logger.Info("Starting dummy export job")
|
|
|
|
// Select all dashboards
|
|
|
|
rowUser := &user.SignedInUser{
|
2022-11-10 13:16:31 -06:00
|
|
|
Login: "",
|
2022-10-19 19:05:51 -05:00
|
|
|
OrgID: 0, // gets filled in from each row
|
|
|
|
UserID: 0,
|
|
|
|
}
|
2022-11-10 13:16:31 -06:00
|
|
|
ctx = store.ContextWithUser(ctx, rowUser)
|
2022-10-19 19:05:51 -05:00
|
|
|
|
|
|
|
what := models.StandardKindDashboard
|
|
|
|
e.status.Count[what] = 0
|
|
|
|
|
|
|
|
// TODO paging etc
|
|
|
|
// NOTE: doing work inside rows.Next() leads to database locked
|
|
|
|
dashInfo, err := e.getDashboards(ctx)
|
|
|
|
if err != nil {
|
|
|
|
e.status.Status = "error: " + err.Error()
|
|
|
|
return
|
|
|
|
}
|
2022-11-03 18:35:20 -05:00
|
|
|
e.status.Last = fmt.Sprintf("export %d dashboards", len(dashInfo))
|
|
|
|
e.broadcaster(e.status)
|
2022-10-19 19:05:51 -05:00
|
|
|
|
|
|
|
for _, dash := range dashInfo {
|
|
|
|
rowUser.OrgID = dash.OrgID
|
|
|
|
rowUser.UserID = dash.UpdatedBy
|
|
|
|
if dash.UpdatedBy < 0 {
|
|
|
|
rowUser.UserID = 0 // avoid Uint64Val issue????
|
|
|
|
}
|
|
|
|
|
2022-11-12 13:36:18 -06:00
|
|
|
_, err = e.store.AdminWrite(ctx, &object.AdminWriteObjectRequest{
|
2022-10-31 09:26:16 -05:00
|
|
|
GRN: &object.GRN{
|
|
|
|
Scope: models.ObjectStoreScopeEntity,
|
|
|
|
UID: dash.UID,
|
|
|
|
Kind: models.StandardKindDashboard,
|
|
|
|
},
|
2022-11-12 13:36:18 -06:00
|
|
|
ClearHistory: true,
|
|
|
|
Version: fmt.Sprintf("%d", dash.Version),
|
|
|
|
CreatedAt: dash.Created.UnixMilli(),
|
|
|
|
UpdatedAt: dash.Updated.UnixMilli(),
|
|
|
|
UpdatedBy: fmt.Sprintf("user:%d", dash.UpdatedBy),
|
|
|
|
CreatedBy: fmt.Sprintf("user:%d", dash.CreatedBy),
|
|
|
|
Origin: "export-from-sql",
|
|
|
|
Body: dash.Data,
|
|
|
|
Comment: "(exported from SQL)",
|
2022-10-19 19:05:51 -05:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
e.status.Status = "error: " + err.Error()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
e.status.Changed = time.Now().UnixMilli()
|
|
|
|
e.status.Index++
|
|
|
|
e.status.Count[what] += 1
|
|
|
|
e.status.Last = fmt.Sprintf("ITEM: %s", dash.UID)
|
|
|
|
e.broadcaster(e.status)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Playlists
|
|
|
|
what = models.StandardKindPlaylist
|
|
|
|
e.status.Count[what] = 0
|
|
|
|
rowUser.OrgID = 1
|
|
|
|
rowUser.UserID = 1
|
|
|
|
res, err := e.playlistService.Search(ctx, &playlist.GetPlaylistsQuery{
|
|
|
|
OrgId: rowUser.OrgID, // TODO... all or orgs
|
|
|
|
Limit: 5000,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
e.status.Status = "error: " + err.Error()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
for _, item := range res {
|
|
|
|
playlist, err := e.playlistService.Get(ctx, &playlist.GetPlaylistByUidQuery{
|
|
|
|
UID: item.UID,
|
|
|
|
OrgId: rowUser.OrgID,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
e.status.Status = "error: " + err.Error()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = e.store.Write(ctx, &object.WriteObjectRequest{
|
2022-10-31 09:26:16 -05:00
|
|
|
GRN: &object.GRN{
|
|
|
|
Scope: models.ObjectStoreScopeEntity,
|
|
|
|
UID: playlist.Uid,
|
|
|
|
Kind: models.StandardKindPlaylist,
|
|
|
|
},
|
2022-10-19 19:05:51 -05:00
|
|
|
Body: prettyJSON(playlist),
|
|
|
|
Comment: "export from playlists",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
e.status.Status = "error: " + err.Error()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
e.status.Changed = time.Now().UnixMilli()
|
|
|
|
e.status.Index++
|
|
|
|
e.status.Count[what] += 1
|
|
|
|
e.status.Last = fmt.Sprintf("ITEM: %s", playlist.Uid)
|
|
|
|
e.broadcaster(e.status)
|
|
|
|
}
|
2022-11-01 10:28:13 -05:00
|
|
|
|
|
|
|
// TODO.. query lookup
|
|
|
|
orgIDs := []int64{1}
|
|
|
|
what = "snapshot"
|
|
|
|
for _, orgId := range orgIDs {
|
2022-11-10 13:16:31 -06:00
|
|
|
rowUser.OrgID = orgId
|
|
|
|
rowUser.UserID = 1
|
2022-11-01 10:28:13 -05:00
|
|
|
cmd := &dashboardsnapshots.GetDashboardSnapshotsQuery{
|
|
|
|
OrgId: orgId,
|
|
|
|
Limit: 500000,
|
2022-11-10 13:16:31 -06:00
|
|
|
SignedInUser: rowUser,
|
2022-11-01 10:28:13 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
err := e.dashboardsnapshots.SearchDashboardSnapshots(ctx, cmd)
|
|
|
|
if err != nil {
|
|
|
|
e.status.Status = "error: " + err.Error()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, dto := range cmd.Result {
|
|
|
|
m := snapshot.Model{
|
|
|
|
Name: dto.Name,
|
|
|
|
ExternalURL: dto.ExternalUrl,
|
|
|
|
Expires: dto.Expires.UnixMilli(),
|
|
|
|
}
|
|
|
|
rowUser.OrgID = dto.OrgId
|
|
|
|
rowUser.UserID = dto.UserId
|
|
|
|
|
|
|
|
snapcmd := &dashboardsnapshots.GetDashboardSnapshotQuery{
|
|
|
|
Key: dto.Key,
|
|
|
|
}
|
|
|
|
err = e.dashboardsnapshots.GetDashboardSnapshot(ctx, snapcmd)
|
|
|
|
if err == nil {
|
|
|
|
res := snapcmd.Result
|
|
|
|
m.DeleteKey = res.DeleteKey
|
|
|
|
m.ExternalURL = res.ExternalUrl
|
|
|
|
|
|
|
|
snap := res.Dashboard
|
|
|
|
m.DashboardUID = snap.Get("uid").MustString("")
|
|
|
|
snap.Del("uid")
|
|
|
|
snap.Del("id")
|
|
|
|
|
|
|
|
b, _ := snap.MarshalJSON()
|
|
|
|
m.Snapshot = b
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = e.store.Write(ctx, &object.WriteObjectRequest{
|
|
|
|
GRN: &object.GRN{
|
|
|
|
Scope: models.ObjectStoreScopeEntity,
|
|
|
|
UID: dto.Key,
|
|
|
|
Kind: models.StandardKindSnapshot,
|
|
|
|
},
|
|
|
|
Body: prettyJSON(m),
|
|
|
|
Comment: "export from snapshtts",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
e.status.Status = "error: " + err.Error()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
e.status.Changed = time.Now().UnixMilli()
|
|
|
|
e.status.Index++
|
|
|
|
e.status.Count[what] += 1
|
|
|
|
e.status.Last = fmt.Sprintf("ITEM: %s", dto.Name)
|
|
|
|
e.broadcaster(e.status)
|
|
|
|
}
|
|
|
|
}
|
2022-10-19 19:05:51 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
type dashInfo struct {
|
2022-11-12 13:36:18 -06:00
|
|
|
OrgID int64 `db:"org_id"`
|
2022-10-19 19:05:51 -05:00
|
|
|
UID string
|
2022-11-12 13:36:18 -06:00
|
|
|
Version int64
|
|
|
|
Slug string
|
|
|
|
Data []byte
|
|
|
|
Created time.Time
|
|
|
|
Updated time.Time
|
|
|
|
CreatedBy int64 `db:"created_by"`
|
|
|
|
UpdatedBy int64 `db:"updated_by"`
|
2022-10-19 19:05:51 -05:00
|
|
|
}
|
|
|
|
|
2022-11-12 13:36:18 -06:00
|
|
|
// TODO, paging etc
|
2022-10-19 19:05:51 -05:00
|
|
|
func (e *objectStoreJob) getDashboards(ctx context.Context) ([]dashInfo, error) {
|
|
|
|
e.status.Last = "find dashbaords...."
|
|
|
|
e.broadcaster(e.status)
|
|
|
|
|
|
|
|
dash := make([]dashInfo, 0)
|
2022-11-12 13:36:18 -06:00
|
|
|
err := e.sess.Select(ctx, &dash, "SELECT org_id,uid,version,slug,data,created,updated,created_by,updated_by FROM dashboard WHERE is_folder=false")
|
|
|
|
return dash, err
|
2022-10-19 19:05:51 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func (e *objectStoreJob) getStatus() ExportStatus {
|
|
|
|
e.statusMu.Lock()
|
|
|
|
defer e.statusMu.Unlock()
|
|
|
|
|
|
|
|
return e.status
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *objectStoreJob) getConfig() ExportConfig {
|
|
|
|
e.statusMu.Lock()
|
|
|
|
defer e.statusMu.Unlock()
|
|
|
|
|
|
|
|
return e.cfg
|
|
|
|
}
|