Export: save all dashboards to git (#48233)
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
type commitHelper struct {
|
||||
ctx context.Context
|
||||
repo *git.Repository
|
||||
work *git.Worktree
|
||||
orgDir string // includes the orgID
|
||||
workDir string // same as the worktree root
|
||||
orgID int64
|
||||
users map[int64]*userInfo
|
||||
}
|
||||
|
||||
type commitBody struct {
|
||||
fpath string // absolute
|
||||
body []byte
|
||||
frame *data.Frame
|
||||
}
|
||||
|
||||
type commitOptions struct {
|
||||
body []commitBody
|
||||
when time.Time
|
||||
userID int64
|
||||
comment string
|
||||
}
|
||||
|
||||
func (ch *commitHelper) initOrg(sql *sqlstore.SQLStore, orgID int64) error {
|
||||
return sql.WithDbSession(ch.ctx, func(sess *sqlstore.DBSession) error {
|
||||
sess.Table("user").
|
||||
Join("inner", "org_user", "user.id = org_user.user_id").
|
||||
Cols("user.*", "org_user.role").
|
||||
Where("org_user.org_id = ?", orgID).
|
||||
Asc("user.id")
|
||||
|
||||
rows := make([]*userInfo, 0)
|
||||
err := sess.Find(&rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lookup := make(map[int64]*userInfo, len(rows))
|
||||
for _, row := range rows {
|
||||
lookup[row.ID] = row
|
||||
}
|
||||
ch.users = lookup
|
||||
ch.orgID = orgID
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (ch *commitHelper) add(opts commitOptions) error {
|
||||
for _, b := range opts.body {
|
||||
if !strings.HasPrefix(b.fpath, ch.orgDir) {
|
||||
return fmt.Errorf("invalid path, must be within the root folder")
|
||||
}
|
||||
|
||||
// make sure the parent exists
|
||||
err := os.MkdirAll(path.Dir(b.fpath), 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body := b.body
|
||||
if b.frame != nil {
|
||||
body, err = jsoniter.ConfigCompatibleWithStandardLibrary.MarshalIndent(b.frame, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(b.fpath, body, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sub := b.fpath[len(ch.workDir)+1:]
|
||||
_, err = ch.work.Add(sub)
|
||||
if err != nil {
|
||||
status, e2 := ch.work.Status()
|
||||
if e2 != nil {
|
||||
return fmt.Errorf("error adding: %s (invalud work status: %s)", sub, e2.Error())
|
||||
}
|
||||
fmt.Printf("STATUS: %+v\n", status)
|
||||
return fmt.Errorf("unable to add file: %s (%d)", sub, len(b.body))
|
||||
}
|
||||
}
|
||||
|
||||
user, ok := ch.users[opts.userID]
|
||||
if !ok {
|
||||
user = &userInfo{
|
||||
Name: "admin",
|
||||
Email: "admin@unknown.org",
|
||||
}
|
||||
}
|
||||
sig := user.getAuthor()
|
||||
if opts.when.Unix() > 10 {
|
||||
sig.When = opts.when
|
||||
}
|
||||
|
||||
copts := &git.CommitOptions{
|
||||
Author: &sig,
|
||||
}
|
||||
|
||||
_, err := ch.work.Commit(opts.comment, copts)
|
||||
return err
|
||||
}
|
||||
|
||||
type userInfo struct {
|
||||
ID int64 `json:"-" xorm:"id"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
Salt string `json:"salt"`
|
||||
Role string `json:"org_role"` // org role
|
||||
Theme string `json:"-"` // managed in preferences
|
||||
Created time.Time `json:"-"` // managed in git or external source
|
||||
Updated time.Time `json:"-"` // managed in git or external source
|
||||
IsDisabled bool `json:"disabled" xorm:"is_disabled"`
|
||||
IsServiceAccount bool `json:"serviceAccount" xorm:"is_service_account"`
|
||||
LastSeenAt time.Time `json:"-" xorm:"last_seen_at"`
|
||||
}
|
||||
|
||||
func (u *userInfo) getAuthor() object.Signature {
|
||||
return object.Signature{
|
||||
Name: firstRealStringX(u.Name, u.Login, u.Email, "?"),
|
||||
Email: firstRealStringX(u.Email, u.Login, u.Name, "?"),
|
||||
}
|
||||
}
|
||||
|
||||
func firstRealStringX(vals ...string) string {
|
||||
for _, v := range vals {
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
@@ -23,10 +22,6 @@ type dummyExportJob struct {
|
||||
}
|
||||
|
||||
func startDummyExportJob(cfg ExportConfig, broadcaster statusBroadcaster) (Job, error) {
|
||||
if cfg.Format != "git" {
|
||||
return nil, errors.New("only git format is supported")
|
||||
}
|
||||
|
||||
job := &dummyExportJob{
|
||||
logger: log.New("dummy_export_job"),
|
||||
cfg: cfg,
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
func exportAnnotations(helper *commitHelper, job *gitExportJob) error {
|
||||
return job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error {
|
||||
type annoResult struct {
|
||||
ID int64 `xorm:"id"`
|
||||
DashboardID int64 `xorm:"dashboard_id"`
|
||||
PanelID int64 `xorm:"panel_id"`
|
||||
UserID int64 `xorm:"user_id"`
|
||||
Text string `xorm:"text"`
|
||||
Epoch int64 `xorm:"epoch"`
|
||||
EpochEnd int64 `xorm:"epoch_end"`
|
||||
Created int64 `xorm:"created"` // not used
|
||||
Tags string `xorm:"tags"` // JSON Array
|
||||
}
|
||||
|
||||
type annoEvent struct {
|
||||
PanelID int64 `json:"panel"`
|
||||
Text string `json:"text"`
|
||||
Epoch int64 `json:"epoch"` // dashboard/start+end is really the UID
|
||||
EpochEnd int64 `json:"epoch_end,omitempty"`
|
||||
Tags []string
|
||||
}
|
||||
|
||||
rows := make([]*annoResult, 0)
|
||||
|
||||
sess.Table("annotation").
|
||||
Where("org_id = ? AND alert_id = 0", helper.orgID).Asc("epoch")
|
||||
|
||||
err := sess.Find(&rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count := len(rows)
|
||||
f_ID := data.NewFieldFromFieldType(data.FieldTypeInt64, count)
|
||||
f_DashboardID := data.NewFieldFromFieldType(data.FieldTypeInt64, count)
|
||||
f_PanelID := data.NewFieldFromFieldType(data.FieldTypeInt64, count)
|
||||
f_Epoch := data.NewFieldFromFieldType(data.FieldTypeTime, count)
|
||||
f_EpochEnd := data.NewFieldFromFieldType(data.FieldTypeNullableTime, count)
|
||||
f_Text := data.NewFieldFromFieldType(data.FieldTypeString, count)
|
||||
f_Tags := data.NewFieldFromFieldType(data.FieldTypeJSON, count)
|
||||
|
||||
f_ID.Name = "ID"
|
||||
f_DashboardID.Name = "DashboardID"
|
||||
f_PanelID.Name = "PanelID"
|
||||
f_Epoch.Name = "Epoch"
|
||||
f_EpochEnd.Name = "EpochEnd"
|
||||
f_Text.Name = "Text"
|
||||
f_Tags.Name = "Tags"
|
||||
|
||||
for id, row := range rows {
|
||||
f_ID.Set(id, row.ID)
|
||||
f_DashboardID.Set(id, row.DashboardID)
|
||||
f_PanelID.Set(id, row.PanelID)
|
||||
f_Epoch.Set(id, time.UnixMilli(row.Epoch))
|
||||
if row.Epoch != row.EpochEnd {
|
||||
f_EpochEnd.SetConcrete(id, time.UnixMilli(row.EpochEnd))
|
||||
}
|
||||
f_Text.Set(id, row.Text)
|
||||
f_Tags.Set(id, json.RawMessage(row.Tags))
|
||||
|
||||
// Save a file for each
|
||||
event := &annoEvent{
|
||||
PanelID: row.PanelID,
|
||||
Text: row.Text,
|
||||
}
|
||||
err = json.Unmarshal([]byte(row.Tags), &event.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fname := fmt.Sprintf("%d", row.Epoch)
|
||||
if row.Epoch != row.EpochEnd {
|
||||
fname += "-" + fmt.Sprintf("%d", row.EpochEnd)
|
||||
}
|
||||
|
||||
err = helper.add(commitOptions{
|
||||
body: []commitBody{
|
||||
{
|
||||
fpath: filepath.Join(helper.orgDir,
|
||||
"annotations",
|
||||
"dashboard",
|
||||
fmt.Sprintf("id-%d", row.DashboardID),
|
||||
fname+".json"),
|
||||
body: prettyJSON(event),
|
||||
},
|
||||
},
|
||||
when: time.UnixMilli(row.Epoch),
|
||||
comment: fmt.Sprintf("Added annotation (%d)", row.ID),
|
||||
userID: row.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
frame := data.NewFrame("", f_ID, f_DashboardID, f_PanelID, f_Epoch, f_EpochEnd, f_Text, f_Tags)
|
||||
js, err := jsoniter.ConfigCompatibleWithStandardLibrary.MarshalIndent(frame, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = helper.add(commitOptions{
|
||||
body: []commitBody{
|
||||
{
|
||||
fpath: filepath.Join(helper.orgDir, "annotations", "annotations.json"),
|
||||
body: js, // TODO, pretty?
|
||||
},
|
||||
},
|
||||
when: time.Now(),
|
||||
comment: "Exported annotations",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
func dumpAuthTables(helper *commitHelper, job *gitExportJob) error {
|
||||
return job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error {
|
||||
commit := commitOptions{
|
||||
comment: "auth tables dump",
|
||||
}
|
||||
|
||||
tables := []string{
|
||||
"user", // joined with "org_user" to get the role
|
||||
"user_role",
|
||||
"builtin_role",
|
||||
"api_key",
|
||||
"team", "team_group", "team_role", "team_member",
|
||||
"role",
|
||||
"temp_user",
|
||||
"user_auth_token", // no org_id... is it temporary?
|
||||
"permission",
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
switch table {
|
||||
case "permission":
|
||||
sess.Table(table).
|
||||
Join("left", "role", "permission.role_id = role.id").
|
||||
Cols("permission.*").
|
||||
Where("org_id = ?", helper.orgID).
|
||||
Asc("permission.id")
|
||||
case "user":
|
||||
sess.Table(table).
|
||||
Join("inner", "org_user", "user.id = org_user.user_id").
|
||||
Cols("user.*", "org_user.role").
|
||||
Where("org_user.org_id = ?", helper.orgID).
|
||||
Asc("user.id")
|
||||
case "user_auth_token":
|
||||
sess.Table(table).
|
||||
Join("inner", "org_user", "user_auth_token.id = org_user.user_id").
|
||||
Cols("user_auth_token.*").
|
||||
Where("org_user.org_id = ?", helper.orgID).
|
||||
Asc("user_auth_token.id")
|
||||
default:
|
||||
sess.Table(table).Where("org_id = ?", helper.orgID).Asc("id")
|
||||
}
|
||||
|
||||
raw, err := sess.QueryInterface()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read: %s // %s", table, err.Error())
|
||||
}
|
||||
if len(raw) < 1 {
|
||||
continue // don't write empty files
|
||||
}
|
||||
frame, err := queryResultToDataFrame(raw, frameOpts{
|
||||
skip: []string{"org_id", "version", "help_flags1", "theme"},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
frame.Name = table
|
||||
commit.body = append(commit.body, commitBody{
|
||||
fpath: path.Join(helper.orgDir, "auth", "sql.dump", table+".json"),
|
||||
frame: frame,
|
||||
})
|
||||
}
|
||||
return helper.add(commit)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/grafana/grafana/pkg/infra/filestorage"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2/extract"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
func exportDashboards(helper *commitHelper, job *gitExportJob, lookup dsLookup) error {
|
||||
alias := make(map[string]string, 100)
|
||||
ids := make(map[int64]string, 100)
|
||||
folders := make(map[int64]string, 100)
|
||||
|
||||
// Should root files be at the root or in a subfolder called "general"?
|
||||
if true {
|
||||
folders[0] = "general"
|
||||
}
|
||||
|
||||
rootDir := path.Join(helper.orgDir, "root")
|
||||
folderStructure := commitOptions{
|
||||
when: time.Now(),
|
||||
comment: "Exported folder structure",
|
||||
}
|
||||
|
||||
err := job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error {
|
||||
type dashDataQueryResult struct {
|
||||
Id int64
|
||||
UID string `xorm:"uid"`
|
||||
IsFolder bool `xorm:"is_folder"`
|
||||
FolderID int64 `xorm:"folder_id"`
|
||||
Slug string `xorm:"slug"`
|
||||
Data []byte
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
rows := make([]*dashDataQueryResult, 0)
|
||||
|
||||
sess.Table("dashboard").
|
||||
Where("org_id = ?", helper.orgID).
|
||||
Cols("id", "is_folder", "folder_id", "data", "slug", "created", "updated", "uid")
|
||||
|
||||
err := sess.Find(&rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Process all folders
|
||||
for _, row := range rows {
|
||||
if !row.IsFolder {
|
||||
continue
|
||||
}
|
||||
dash, err := extract.ReadDashboard(bytes.NewReader(row.Data), lookup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dash.UID = row.UID
|
||||
slug := cleanFileName(dash.Title)
|
||||
folder := map[string]string{
|
||||
"title": dash.Title,
|
||||
}
|
||||
|
||||
folderStructure.body = append(folderStructure.body, commitBody{
|
||||
fpath: path.Join(rootDir, slug, "__folder.json"),
|
||||
body: prettyJSON(folder),
|
||||
})
|
||||
|
||||
alias[dash.UID] = slug
|
||||
folders[row.Id] = slug
|
||||
|
||||
if row.Created.Before(folderStructure.when) {
|
||||
folderStructure.when = row.Created
|
||||
}
|
||||
}
|
||||
|
||||
// Now process the dashboards in each folder
|
||||
for _, row := range rows {
|
||||
if row.IsFolder {
|
||||
continue
|
||||
}
|
||||
fname := row.Slug + "-dash.json"
|
||||
fpath, ok := folders[row.FolderID]
|
||||
if ok {
|
||||
fpath = path.Join(fpath, fname)
|
||||
} else {
|
||||
fpath = fname
|
||||
}
|
||||
|
||||
alias[row.UID] = fpath
|
||||
ids[row.Id] = fpath
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = helper.add(folderStructure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = helper.add(commitOptions{
|
||||
body: []commitBody{
|
||||
{
|
||||
fpath: filepath.Join(helper.orgDir, "root-alias.json"),
|
||||
body: prettyJSON(alias),
|
||||
},
|
||||
{
|
||||
fpath: filepath.Join(helper.orgDir, "root-ids.json"),
|
||||
body: prettyJSON(ids),
|
||||
},
|
||||
},
|
||||
when: folderStructure.when,
|
||||
comment: "adding UID alias structure",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now walk the history
|
||||
err = job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error {
|
||||
type dashVersionResult struct {
|
||||
DashId int64 `xorm:"dashboard_id"`
|
||||
Version int64 `xorm:"version"`
|
||||
Created time.Time `xorm:"created"`
|
||||
CreatedBy int64 `xorm:"created_by"`
|
||||
Message string `xorm:"message"`
|
||||
Data []byte
|
||||
}
|
||||
|
||||
rows := make([]*dashVersionResult, 0, len(ids))
|
||||
|
||||
sess.Table("dashboard_version").
|
||||
Join("INNER", "dashboard", "dashboard.id = dashboard_version.dashboard_id").
|
||||
Where("org_id = ?", job.orgID).
|
||||
Cols("dashboard_version.dashboard_id",
|
||||
"dashboard_version.version",
|
||||
"dashboard_version.created",
|
||||
"dashboard_version.created_by",
|
||||
"dashboard_version.message",
|
||||
"dashboard_version.data").
|
||||
Asc("dashboard_version.created")
|
||||
|
||||
err := sess.Find(&rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count := int64(0)
|
||||
|
||||
// Process all folders (only one level deep!!!)
|
||||
for _, row := range rows {
|
||||
fpath, ok := ids[row.DashId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
msg := row.Message
|
||||
if msg == "" {
|
||||
msg = fmt.Sprintf("Version: %d", row.Version)
|
||||
}
|
||||
|
||||
err = helper.add(commitOptions{
|
||||
body: []commitBody{
|
||||
{
|
||||
fpath: filepath.Join(rootDir, fpath),
|
||||
body: cleanDashboardJSON(row.Data),
|
||||
},
|
||||
},
|
||||
userID: row.CreatedBy,
|
||||
when: row.Created,
|
||||
comment: msg,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count++
|
||||
fmt.Printf("COMMIT: %d // %s (%d)\n", count, fpath, row.Version)
|
||||
|
||||
job.status.Current = count
|
||||
job.status.Last = fpath
|
||||
job.status.Changed = time.Now().UnixMilli()
|
||||
job.broadcaster(job.status)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func cleanDashboardJSON(data []byte) []byte {
|
||||
var dash map[string]interface{}
|
||||
err := json.Unmarshal(data, &dash)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
delete(dash, "id")
|
||||
delete(dash, "uid")
|
||||
delete(dash, "version")
|
||||
|
||||
clean, _ := json.MarshalIndent(dash, "", " ")
|
||||
return clean
|
||||
}
|
||||
|
||||
// replace any unsafe file name characters... TODO, but be a standard way to do this cleanly!!!
|
||||
func cleanFileName(name string) string {
|
||||
name = strings.ReplaceAll(name, "/", "-")
|
||||
name = strings.ReplaceAll(name, "\\", "-")
|
||||
name = strings.ReplaceAll(name, ":", "-")
|
||||
if err := filestorage.ValidatePath(filestorage.Delimiter + name); err != nil {
|
||||
randomName, _ := uuid.NewRandom()
|
||||
return randomName.String()
|
||||
}
|
||||
return name
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2/extract"
|
||||
)
|
||||
|
||||
type dsLookup func(ref *extract.DataSourceRef) *extract.DataSourceRef
|
||||
|
||||
func exportDataSources(helper *commitHelper, job *gitExportJob) (dsLookup, error) {
|
||||
cmd := &datasources.GetDataSourcesQuery{
|
||||
OrgId: job.orgID,
|
||||
}
|
||||
err := job.sql.GetDataSources(helper.ctx, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.SliceStable(cmd.Result, func(i, j int) bool {
|
||||
return cmd.Result[i].Created.After(cmd.Result[j].Created)
|
||||
})
|
||||
|
||||
byUID := make(map[string]*extract.DataSourceRef, len(cmd.Result))
|
||||
byName := make(map[string]*extract.DataSourceRef, len(cmd.Result))
|
||||
for _, ds := range cmd.Result {
|
||||
ref := &extract.DataSourceRef{
|
||||
UID: ds.Uid,
|
||||
Type: ds.Type,
|
||||
}
|
||||
byUID[ds.Uid] = ref
|
||||
byName[ds.Name] = ref
|
||||
ds.OrgId = 0
|
||||
ds.Version = 0
|
||||
|
||||
err := helper.add(commitOptions{
|
||||
body: []commitBody{
|
||||
{
|
||||
fpath: filepath.Join(helper.orgDir, "datasources", fmt.Sprintf("%s-ds.json", ds.Uid)),
|
||||
body: prettyJSON(ds),
|
||||
},
|
||||
},
|
||||
when: ds.Created,
|
||||
comment: fmt.Sprintf("Add datasource: %s", ds.Name),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Return the lookup function
|
||||
return func(ref *extract.DataSourceRef) *extract.DataSourceRef {
|
||||
if ref == nil || ref.UID == "" {
|
||||
return &extract.DataSourceRef{
|
||||
UID: "default.uid",
|
||||
Type: "default.type",
|
||||
}
|
||||
}
|
||||
v, ok := byUID[ref.UID]
|
||||
if ok {
|
||||
return v
|
||||
}
|
||||
v, ok = byName[ref.UID]
|
||||
if ok {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||
)
|
||||
|
||||
func exportSnapshots(helper *commitHelper, job *gitExportJob) error {
|
||||
cmd := &dashboardsnapshots.GetDashboardSnapshotsQuery{
|
||||
OrgId: job.orgID,
|
||||
Limit: 500000,
|
||||
SignedInUser: nil,
|
||||
}
|
||||
if cmd.SignedInUser == nil {
|
||||
return fmt.Errorf("snapshots requires an admin user")
|
||||
}
|
||||
|
||||
err := job.dashboardsnapshotsService.SearchDashboardSnapshots(helper.ctx, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(cmd.Result) < 1 {
|
||||
return nil // nothing
|
||||
}
|
||||
|
||||
gitcmd := commitOptions{
|
||||
when: time.Now(),
|
||||
comment: "Export playlists",
|
||||
}
|
||||
|
||||
for _, snapshot := range cmd.Result {
|
||||
gitcmd.body = append(gitcmd.body, commitBody{
|
||||
fpath: filepath.Join(helper.orgDir, "snapshot", fmt.Sprintf("%d-snapshot.json", snapshot.Id)),
|
||||
body: prettyJSON(snapshot),
|
||||
})
|
||||
}
|
||||
|
||||
return helper.add(gitcmd)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func exportSystemPlaylists(helper *commitHelper, job *gitExportJob) error {
|
||||
cmd := &models.GetPlaylistsQuery{
|
||||
OrgId: job.orgID,
|
||||
Limit: 500000,
|
||||
}
|
||||
err := job.sql.SearchPlaylists(helper.ctx, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(cmd.Result) < 1 {
|
||||
return nil // nothing
|
||||
}
|
||||
|
||||
gitcmd := commitOptions{
|
||||
when: time.Now(),
|
||||
comment: "Export playlists",
|
||||
}
|
||||
|
||||
for _, playlist := range cmd.Result {
|
||||
// TODO: fix the playlist API so it returns the json we need :)
|
||||
|
||||
gitcmd.body = append(gitcmd.body, commitBody{
|
||||
fpath: filepath.Join(helper.orgDir, "system", "playlists", fmt.Sprintf("%s-playlist.json", playlist.UID)),
|
||||
body: prettyJSON(playlist),
|
||||
})
|
||||
}
|
||||
|
||||
return helper.add(gitcmd)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
func exportSystemPreferences(helper *commitHelper, job *gitExportJob) error {
|
||||
type preferences struct {
|
||||
UserID int64 `json:"-" xorm:"user_id"`
|
||||
TeamID int64 `json:"-" xorm:"team_id"`
|
||||
HomeDashboardID int64 `json:"-" xorm:"home_dashboard_id"`
|
||||
Updated time.Time `json:"-" xorm:"updated"`
|
||||
JSONData map[string]interface{} `json:"-" xorm:"json_data"`
|
||||
|
||||
Theme string `json:"theme"`
|
||||
Locale string `json:"locale"`
|
||||
Timezone string `json:"timezone"`
|
||||
WeekStart string `json:"week_start,omitempty"`
|
||||
HomeDashboard string `json:"home,omitempty" xorm:"uid"` // dashboard
|
||||
NavBar interface{} `json:"navbar,omitempty"`
|
||||
QueryHistory interface{} `json:"queryHistory,omitempty"`
|
||||
}
|
||||
|
||||
prefsDir := path.Join(helper.orgDir, "system", "preferences")
|
||||
users := make(map[int64]*userInfo, len(helper.users))
|
||||
for _, user := range helper.users {
|
||||
users[user.ID] = user
|
||||
}
|
||||
|
||||
return job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error {
|
||||
rows := make([]*preferences, 0)
|
||||
|
||||
sess.Table("preferences").
|
||||
Join("LEFT", "dashboard", "dashboard.id = preferences.home_dashboard_id").
|
||||
Cols("preferences.*", "dashboard.uid").
|
||||
Where("preferences.org_id = ?", helper.orgID)
|
||||
|
||||
err := sess.Find(&rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var comment string
|
||||
var fpath string
|
||||
for _, row := range rows {
|
||||
if row.TeamID > 0 {
|
||||
fpath = filepath.Join(prefsDir, "team", fmt.Sprintf("%d.json", row.TeamID))
|
||||
comment = fmt.Sprintf("Team preferences: %d", row.TeamID)
|
||||
} else if row.UserID == 0 {
|
||||
fpath = filepath.Join(prefsDir, "default.json")
|
||||
comment = "Default preferences"
|
||||
} else {
|
||||
user, ok := users[row.UserID]
|
||||
if ok {
|
||||
delete(users, row.UserID)
|
||||
} else {
|
||||
user = &userInfo{
|
||||
Login: fmt.Sprintf("__%d__", row.UserID),
|
||||
}
|
||||
}
|
||||
fpath = filepath.Join(prefsDir, "user", fmt.Sprintf("%s.json", user.Login))
|
||||
comment = fmt.Sprintf("User preferences: %s", user.getAuthor().Name)
|
||||
}
|
||||
|
||||
if row.JSONData != nil {
|
||||
v, ok := row.JSONData["locale"]
|
||||
if ok && row.Locale == "" {
|
||||
s, ok := v.(string)
|
||||
if ok {
|
||||
row.Locale = s
|
||||
}
|
||||
}
|
||||
|
||||
v, ok = row.JSONData["navbar"]
|
||||
if ok && row.NavBar == nil {
|
||||
row.NavBar = v
|
||||
}
|
||||
|
||||
v, ok = row.JSONData["queryHistory"]
|
||||
if ok && row.QueryHistory == nil {
|
||||
row.QueryHistory = v
|
||||
}
|
||||
}
|
||||
|
||||
err := helper.add(commitOptions{
|
||||
body: []commitBody{
|
||||
{
|
||||
fpath: fpath,
|
||||
body: prettyJSON(row),
|
||||
},
|
||||
},
|
||||
when: row.Updated,
|
||||
comment: comment,
|
||||
userID: row.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// add a file for all useres that may not be in the system
|
||||
for _, user := range users {
|
||||
row := preferences{
|
||||
Theme: user.Theme, // never set?
|
||||
}
|
||||
err := helper.add(commitOptions{
|
||||
body: []commitBody{
|
||||
{
|
||||
fpath: filepath.Join(prefsDir, "user", fmt.Sprintf("%s.json", user.Login)),
|
||||
body: prettyJSON(row),
|
||||
},
|
||||
},
|
||||
when: user.Updated,
|
||||
comment: "user preferences",
|
||||
userID: row.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
func exportSystemStars(helper *commitHelper, job *gitExportJob) error {
|
||||
byUser := make(map[int64][]string, 50)
|
||||
|
||||
err := job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error {
|
||||
type starResult struct {
|
||||
User int64 `xorm:"user_id"`
|
||||
UID string `xorm:"uid"`
|
||||
}
|
||||
|
||||
rows := make([]*starResult, 0)
|
||||
|
||||
sess.Table("star").
|
||||
Join("INNER", "dashboard", "dashboard.id = star.dashboard_id").
|
||||
Cols("star.user_id", "dashboard.uid").
|
||||
Where("dashboard.org_id = ?", helper.orgID)
|
||||
|
||||
err := sess.Find(&rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
stars := append(byUser[row.User], fmt.Sprintf("dashboard/%s", row.UID))
|
||||
byUser[row.User] = stars
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for userID, stars := range byUser {
|
||||
user, ok := helper.users[userID]
|
||||
if !ok {
|
||||
user = &userInfo{
|
||||
Login: fmt.Sprintf("__unknown_%d", userID),
|
||||
}
|
||||
}
|
||||
|
||||
err := helper.add(commitOptions{
|
||||
body: []commitBody{
|
||||
{
|
||||
fpath: filepath.Join(helper.orgDir, "system", "stars", fmt.Sprintf("%s.json", user.Login)),
|
||||
body: prettyJSON(stars),
|
||||
},
|
||||
},
|
||||
when: user.Updated,
|
||||
comment: "user preferences",
|
||||
userID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
)
|
||||
|
||||
type fieldInfo struct {
|
||||
Name string
|
||||
Conv data.FieldConverter
|
||||
}
|
||||
|
||||
type frameOpts struct {
|
||||
schema []fieldInfo
|
||||
skip []string
|
||||
}
|
||||
|
||||
func prettyJSON(v interface{}) []byte {
|
||||
b, _ := json.MarshalIndent(v, "", " ")
|
||||
return b
|
||||
}
|
||||
|
||||
func queryResultToDataFrame(rows []map[string]interface{}, opts frameOpts) (*data.Frame, error) {
|
||||
count := len(rows)
|
||||
if count < 1 {
|
||||
return nil, nil // empty frame
|
||||
}
|
||||
|
||||
schema := opts.schema
|
||||
if len(schema) < 1 {
|
||||
skip := make(map[string]bool, len(opts.skip))
|
||||
for _, k := range opts.skip {
|
||||
skip[k] = true
|
||||
}
|
||||
|
||||
for k, v := range rows[0] {
|
||||
if skip[k] {
|
||||
continue
|
||||
}
|
||||
field := fieldInfo{
|
||||
Name: k,
|
||||
Conv: data.FieldConverter{
|
||||
OutputFieldType: data.FieldTypeFor(v),
|
||||
},
|
||||
}
|
||||
if field.Conv.OutputFieldType == data.FieldTypeUnknown {
|
||||
fmt.Printf("UNKNOWN type: %s / %v\n", k, v)
|
||||
continue
|
||||
}
|
||||
|
||||
// Don't write passwords to disk for now!!!!
|
||||
if k == "password" || k == "salt" {
|
||||
field.Conv.Converter = func(v interface{}) (interface{}, error) {
|
||||
return fmt.Sprintf("<%s>", k), nil
|
||||
}
|
||||
}
|
||||
|
||||
schema = append(schema, field)
|
||||
}
|
||||
}
|
||||
|
||||
fields := make([]*data.Field, len(schema))
|
||||
for i, s := range schema {
|
||||
fields[i] = data.NewFieldFromFieldType(s.Conv.OutputFieldType, count)
|
||||
fields[i].Name = s.Name
|
||||
}
|
||||
|
||||
var err error
|
||||
for i, row := range rows {
|
||||
for j, s := range schema {
|
||||
v, ok := row[s.Name]
|
||||
if ok && v != nil {
|
||||
if s.Conv.Converter != nil {
|
||||
v, err = s.Conv.Converter(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting field: %s // %s", s.Name, err.Error())
|
||||
}
|
||||
}
|
||||
fields[j].Set(i, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fields are in random order
|
||||
if len(opts.schema) < 1 {
|
||||
last := []*data.Field{}
|
||||
frame := data.NewFrame("")
|
||||
lookup := make(map[string]*data.Field, len(fields))
|
||||
for _, f := range fields {
|
||||
if f.Name == "id" {
|
||||
frame.Fields = append(frame.Fields, f) // first
|
||||
continue
|
||||
}
|
||||
lookup[f.Name] = f
|
||||
}
|
||||
|
||||
// First items
|
||||
for _, name := range []string{"name", "login", "email", "role", "description", "uid"} {
|
||||
f, ok := lookup[name]
|
||||
if ok {
|
||||
frame.Fields = append(frame.Fields, f) // first
|
||||
delete(lookup, name)
|
||||
}
|
||||
}
|
||||
|
||||
// IDs
|
||||
for k, f := range lookup {
|
||||
if strings.HasSuffix(k, "_id") {
|
||||
frame.Fields = append(frame.Fields, f) // first
|
||||
delete(lookup, k)
|
||||
} else if strings.HasPrefix(k, "is_") {
|
||||
last = append(last, f) // first
|
||||
delete(lookup, k)
|
||||
}
|
||||
}
|
||||
|
||||
// Last items
|
||||
for _, name := range []string{"created", "updated"} {
|
||||
f, ok := lookup[name]
|
||||
if ok {
|
||||
last = append(last, f) // first
|
||||
delete(lookup, name)
|
||||
}
|
||||
}
|
||||
|
||||
// Rest
|
||||
for _, f := range lookup {
|
||||
frame.Fields = append(frame.Fields, f)
|
||||
}
|
||||
|
||||
frame.Fields = append(frame.Fields, last...)
|
||||
return frame, nil
|
||||
}
|
||||
return data.NewFrame("", fields...), nil
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
var _ Job = new(gitExportJob)
|
||||
|
||||
type gitExportJob struct {
|
||||
logger log.Logger
|
||||
sql *sqlstore.SQLStore
|
||||
dashboardsnapshotsService dashboardsnapshots.Service
|
||||
orgID int64
|
||||
rootDir string
|
||||
|
||||
statusMu sync.Mutex
|
||||
status ExportStatus
|
||||
cfg ExportConfig
|
||||
broadcaster statusBroadcaster
|
||||
}
|
||||
|
||||
type simpleExporter = func(helper *commitHelper, job *gitExportJob) error
|
||||
|
||||
func startGitExportJob(cfg ExportConfig, sql *sqlstore.SQLStore, dashboardsnapshotsService dashboardsnapshots.Service, rootDir string, orgID int64, broadcaster statusBroadcaster) (Job, error) {
|
||||
job := &gitExportJob{
|
||||
logger: log.New("git_export_job"),
|
||||
cfg: cfg,
|
||||
sql: sql,
|
||||
dashboardsnapshotsService: dashboardsnapshotsService,
|
||||
orgID: orgID,
|
||||
rootDir: rootDir,
|
||||
broadcaster: broadcaster,
|
||||
status: ExportStatus{
|
||||
Running: true,
|
||||
Target: "git export",
|
||||
Started: time.Now().UnixMilli(),
|
||||
Current: 0,
|
||||
},
|
||||
}
|
||||
|
||||
broadcaster(job.status)
|
||||
go job.start()
|
||||
return job, nil
|
||||
}
|
||||
|
||||
func (e *gitExportJob) getStatus() ExportStatus {
|
||||
e.statusMu.Lock()
|
||||
defer e.statusMu.Unlock()
|
||||
|
||||
return e.status
|
||||
}
|
||||
|
||||
func (e *gitExportJob) getConfig() ExportConfig {
|
||||
e.statusMu.Lock()
|
||||
defer e.statusMu.Unlock()
|
||||
|
||||
return e.cfg
|
||||
}
|
||||
|
||||
// Utility function to export dashboards
|
||||
func (e *gitExportJob) start() {
|
||||
defer func() {
|
||||
e.logger.Info("Finished git 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"
|
||||
}
|
||||
s.Target = e.rootDir
|
||||
e.status = s
|
||||
e.broadcaster(s)
|
||||
}()
|
||||
|
||||
err := e.doExportWithHistory()
|
||||
if err != nil {
|
||||
e.logger.Error("ERROR", "e", err)
|
||||
e.status.Status = "ERROR"
|
||||
e.status.Last = err.Error()
|
||||
e.broadcaster(e.status)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *gitExportJob) doExportWithHistory() error {
|
||||
r, err := git.PlainInit(e.rootDir, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// default to "main" branch
|
||||
h := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.ReferenceName("refs/heads/main"))
|
||||
err = r.Storer.SetReference(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := r.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
helper := &commitHelper{
|
||||
repo: r,
|
||||
work: w,
|
||||
ctx: context.Background(),
|
||||
workDir: e.rootDir,
|
||||
orgDir: e.rootDir,
|
||||
}
|
||||
|
||||
cmd := &models.SearchOrgsQuery{}
|
||||
err = e.sql.SearchOrgs(helper.ctx, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Export each org
|
||||
for _, org := range cmd.Result {
|
||||
if len(cmd.Result) > 1 {
|
||||
helper.orgDir = path.Join(e.rootDir, fmt.Sprintf("org_%d", org.Id))
|
||||
}
|
||||
err = helper.initOrg(e.sql, org.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = e.doOrgExportWithHistory(helper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup the folder
|
||||
e.status.Target = "pruning..."
|
||||
e.broadcaster(e.status)
|
||||
err = r.Prune(git.PruneOptions{})
|
||||
|
||||
// TODO
|
||||
// git gc --prune=now --aggressive
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *gitExportJob) doOrgExportWithHistory(helper *commitHelper) error {
|
||||
lookup, err := exportDataSources(helper, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if true {
|
||||
err = exportDashboards(helper, e, lookup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Run all the simple exporters
|
||||
exporters := []simpleExporter{
|
||||
dumpAuthTables,
|
||||
exportSystemPreferences,
|
||||
exportSystemStars,
|
||||
exportSystemPlaylists,
|
||||
exportAnnotations,
|
||||
}
|
||||
|
||||
// This needs a real admin user to use the interfaces (and decrypt)
|
||||
if false {
|
||||
exporters = append(exporters, exportSnapshots)
|
||||
}
|
||||
|
||||
for _, fn := range exporters {
|
||||
err = fn(helper, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
git remote add origin git@github.com:ryantxu/test-dash-repo.git
|
||||
git branch -M main
|
||||
git push -u origin main
|
||||
|
||||
**/
|
||||
@@ -2,15 +2,21 @@ package export
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/live"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type ExportService interface {
|
||||
@@ -22,25 +28,31 @@ type ExportService interface {
|
||||
}
|
||||
|
||||
type StandardExport struct {
|
||||
logger log.Logger
|
||||
sql *sqlstore.SQLStore
|
||||
glive *live.GrafanaLive
|
||||
mutex sync.Mutex
|
||||
logger log.Logger
|
||||
glive *live.GrafanaLive
|
||||
mutex sync.Mutex
|
||||
dataDir string
|
||||
|
||||
// Services
|
||||
sql *sqlstore.SQLStore
|
||||
dashboardsnapshotsService dashboardsnapshots.Service
|
||||
|
||||
// updated with mutex
|
||||
exportJob Job
|
||||
}
|
||||
|
||||
func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles, gl *live.GrafanaLive) ExportService {
|
||||
func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles, gl *live.GrafanaLive, cfg *setting.Cfg, dashboardsnapshotsService dashboardsnapshots.Service) ExportService {
|
||||
if !features.IsEnabled(featuremgmt.FlagExport) {
|
||||
return &StubExport{}
|
||||
}
|
||||
|
||||
return &StandardExport{
|
||||
sql: sql,
|
||||
glive: gl,
|
||||
logger: log.New("export_service"),
|
||||
exportJob: &stoppedJob{},
|
||||
sql: sql,
|
||||
glive: gl,
|
||||
logger: log.New("export_service"),
|
||||
dashboardsnapshotsService: dashboardsnapshotsService,
|
||||
exportJob: &stoppedJob{},
|
||||
dataDir: cfg.DataPath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +79,23 @@ func (ex *StandardExport) HandleRequestExport(c *models.ReqContext) response.Res
|
||||
return response.Error(http.StatusLocked, "export already running", nil)
|
||||
}
|
||||
|
||||
job, err := startDummyExportJob(cfg, func(s ExportStatus) {
|
||||
var job Job
|
||||
broadcast := func(s ExportStatus) {
|
||||
ex.broadcastStatus(c.OrgId, s)
|
||||
})
|
||||
}
|
||||
switch cfg.Format {
|
||||
case "dummy":
|
||||
job, err = startDummyExportJob(cfg, broadcast)
|
||||
case "git":
|
||||
dir := filepath.Join(ex.dataDir, "export_git", fmt.Sprintf("git_%d", time.Now().Unix()))
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Error creating export folder", nil)
|
||||
}
|
||||
job, err = startGitExportJob(cfg, ex.sql, ex.dashboardsnapshotsService, dir, c.OrgId, broadcast)
|
||||
default:
|
||||
return response.Error(http.StatusBadRequest, "Unsupported job format", nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ex.logger.Error("failed to start export job", "err", err)
|
||||
return response.Error(http.StatusBadRequest, "failed to start export job", err)
|
||||
|
||||
Reference in New Issue
Block a user