mirror of
https://github.com/grafana/grafana.git
synced 2025-09-20 02:18:56 +08:00
chore: remove export service POC from main (#63945)
* chore: remove export service POC from main This is a POC and we'll see it, or something like it, again! * remove frontend changes --------- Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -259,7 +259,6 @@
|
|||||||
/pkg/services/searchV2/ @grafana/multitenancy-squad
|
/pkg/services/searchV2/ @grafana/multitenancy-squad
|
||||||
/pkg/services/store/ @grafana/multitenancy-squad
|
/pkg/services/store/ @grafana/multitenancy-squad
|
||||||
/pkg/services/querylibrary/ @grafana/multitenancy-squad
|
/pkg/services/querylibrary/ @grafana/multitenancy-squad
|
||||||
/pkg/services/export/ @grafana/multitenancy-squad
|
|
||||||
/pkg/infra/filestorage/ @grafana/multitenancy-squad
|
/pkg/infra/filestorage/ @grafana/multitenancy-squad
|
||||||
/pkg/util/converter/ @grafana/multitenancy-squad
|
/pkg/util/converter/ @grafana/multitenancy-squad
|
||||||
|
|
||||||
|
@ -101,7 +101,6 @@ The following toggles require explicitly setting Grafana's [app mode]({{< relref
|
|||||||
| `publicDashboardsEmailSharing` | Allows public dashboard sharing to be restricted to only allowed emails |
|
| `publicDashboardsEmailSharing` | Allows public dashboard sharing to be restricted to only allowed emails |
|
||||||
| `k8s` | Explore native k8s integrations |
|
| `k8s` | Explore native k8s integrations |
|
||||||
| `dashboardsFromStorage` | Load dashboards from the generic storage interface |
|
| `dashboardsFromStorage` | Load dashboards from the generic storage interface |
|
||||||
| `export` | Export grafana instance (to git, etc) |
|
|
||||||
| `grpcServer` | Run GRPC server |
|
| `grpcServer` | Run GRPC server |
|
||||||
| `entityStore` | SQL-based entity store (requires storage flag also) |
|
| `entityStore` | SQL-based entity store (requires storage flag also) |
|
||||||
| `queryLibrary` | Reusable query library |
|
| `queryLibrary` | Reusable query library |
|
||||||
|
@ -37,7 +37,6 @@ export interface FeatureToggles {
|
|||||||
storage?: boolean;
|
storage?: boolean;
|
||||||
k8s?: boolean;
|
k8s?: boolean;
|
||||||
dashboardsFromStorage?: boolean;
|
dashboardsFromStorage?: boolean;
|
||||||
export?: boolean;
|
|
||||||
exploreMixedDatasource?: boolean;
|
exploreMixedDatasource?: boolean;
|
||||||
tracing?: boolean;
|
tracing?: boolean;
|
||||||
newTraceView?: boolean;
|
newTraceView?: boolean;
|
||||||
|
@ -630,13 +630,6 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(hs.AdminGetStats))
|
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(hs.AdminGetStats))
|
||||||
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, routing.Wrap(hs.PauseAllAlerts(setting.AlertingEnabled)))
|
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, routing.Wrap(hs.PauseAllAlerts(setting.AlertingEnabled)))
|
||||||
|
|
||||||
if hs.Features.IsEnabled(featuremgmt.FlagExport) {
|
|
||||||
adminRoute.Get("/export", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleGetStatus))
|
|
||||||
adminRoute.Post("/export", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleRequestExport))
|
|
||||||
adminRoute.Post("/export/stop", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleRequestStop))
|
|
||||||
adminRoute.Get("/export/options", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleGetOptions))
|
|
||||||
}
|
|
||||||
|
|
||||||
adminRoute.Post("/encryption/rotate-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminRotateDataEncryptionKeys))
|
adminRoute.Post("/encryption/rotate-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminRotateDataEncryptionKeys))
|
||||||
adminRoute.Post("/encryption/reencrypt-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptEncryptionKeys))
|
adminRoute.Post("/encryption/reencrypt-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptEncryptionKeys))
|
||||||
adminRoute.Post("/encryption/reencrypt-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptSecrets))
|
adminRoute.Post("/encryption/reencrypt-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptSecrets))
|
||||||
|
@ -52,7 +52,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources/permissions"
|
"github.com/grafana/grafana/pkg/services/datasources/permissions"
|
||||||
"github.com/grafana/grafana/pkg/services/encryption"
|
"github.com/grafana/grafana/pkg/services/encryption"
|
||||||
"github.com/grafana/grafana/pkg/services/export"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/hooks"
|
"github.com/grafana/grafana/pkg/services/hooks"
|
||||||
@ -146,7 +145,6 @@ type HTTPServer struct {
|
|||||||
Live *live.GrafanaLive
|
Live *live.GrafanaLive
|
||||||
LivePushGateway *pushhttp.Gateway
|
LivePushGateway *pushhttp.Gateway
|
||||||
ThumbService thumbs.Service
|
ThumbService thumbs.Service
|
||||||
ExportService export.ExportService
|
|
||||||
StorageService store.StorageService
|
StorageService store.StorageService
|
||||||
httpEntityStore httpentitystore.HTTPEntityStore
|
httpEntityStore httpentitystore.HTTPEntityStore
|
||||||
SearchV2HTTPService searchV2.SearchHTTPService
|
SearchV2HTTPService searchV2.SearchHTTPService
|
||||||
@ -234,7 +232,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
|
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
|
||||||
contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
|
contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
|
||||||
alertNG *ngalert.AlertNG, libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
|
alertNG *ngalert.AlertNG, libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
|
||||||
quotaService quota.Service, socialService social.Service, tracer tracing.Tracer, exportService export.ExportService,
|
quotaService quota.Service, socialService social.Service, tracer tracing.Tracer,
|
||||||
encryptionService encryption.Internal, grafanaUpdateChecker *updatechecker.GrafanaService,
|
encryptionService encryption.Internal, grafanaUpdateChecker *updatechecker.GrafanaService,
|
||||||
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
|
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
|
||||||
dataSourcesService datasources.DataSourceService, queryDataService *query.Service,
|
dataSourcesService datasources.DataSourceService, queryDataService *query.Service,
|
||||||
@ -298,7 +296,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
DataProxy: dataSourceProxy,
|
DataProxy: dataSourceProxy,
|
||||||
SearchV2HTTPService: searchv2HTTPService,
|
SearchV2HTTPService: searchv2HTTPService,
|
||||||
SearchService: searchService,
|
SearchService: searchService,
|
||||||
ExportService: exportService,
|
|
||||||
Live: live,
|
Live: live,
|
||||||
LivePushGateway: livePushGateway,
|
LivePushGateway: livePushGateway,
|
||||||
PluginContextProvider: plugCtxProvider,
|
PluginContextProvider: plugCtxProvider,
|
||||||
|
@ -5,6 +5,7 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||||
|
|
||||||
@ -57,7 +58,6 @@ import (
|
|||||||
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||||
"github.com/grafana/grafana/pkg/services/encryption"
|
"github.com/grafana/grafana/pkg/services/encryption"
|
||||||
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
|
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
|
||||||
"github.com/grafana/grafana/pkg/services/export"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/services/hooks"
|
"github.com/grafana/grafana/pkg/services/hooks"
|
||||||
@ -209,7 +209,6 @@ var wireSet = wire.NewSet(
|
|||||||
search.ProvideService,
|
search.ProvideService,
|
||||||
searchV2.ProvideService,
|
searchV2.ProvideService,
|
||||||
store.ProvideService,
|
store.ProvideService,
|
||||||
export.ProvideService,
|
|
||||||
live.ProvideService,
|
live.ProvideService,
|
||||||
pushhttp.ProvideService,
|
pushhttp.ProvideService,
|
||||||
contexthandler.ProvideService,
|
contexthandler.ProvideService,
|
||||||
|
@ -6,6 +6,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api"
|
"github.com/grafana/grafana/pkg/api"
|
||||||
@ -59,7 +60,6 @@ import (
|
|||||||
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||||
"github.com/grafana/grafana/pkg/services/encryption"
|
"github.com/grafana/grafana/pkg/services/encryption"
|
||||||
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
|
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
|
||||||
"github.com/grafana/grafana/pkg/services/export"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/grpcserver"
|
"github.com/grafana/grafana/pkg/services/grpcserver"
|
||||||
@ -231,7 +231,6 @@ var wireBasicSet = wire.NewSet(
|
|||||||
searchV2.ProvideSearchHTTPService,
|
searchV2.ProvideSearchHTTPService,
|
||||||
store.ProvideService,
|
store.ProvideService,
|
||||||
store.ProvideSystemUsersService,
|
store.ProvideSystemUsersService,
|
||||||
export.ProvideService,
|
|
||||||
live.ProvideService,
|
live.ProvideService,
|
||||||
pushhttp.ProvideService,
|
pushhttp.ProvideService,
|
||||||
contexthandler.ProvideService,
|
contexthandler.ProvideService,
|
||||||
|
@ -1,196 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"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"
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/appcontext"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
stopRequested bool
|
|
||||||
broadcast func(path string)
|
|
||||||
exporter string // key for the current exporter
|
|
||||||
|
|
||||||
counter int
|
|
||||||
}
|
|
||||||
|
|
||||||
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(ctx context.Context, sql db.DB, orgID int64) error {
|
|
||||||
return sql.WithDbSession(ch.ctx, func(sess *db.Session) error {
|
|
||||||
userprefix := "user"
|
|
||||||
if isPostgreSQL(sql) {
|
|
||||||
userprefix = `"user"` // postgres has special needs
|
|
||||||
}
|
|
||||||
sess.Table("user").
|
|
||||||
Join("inner", "org_user", userprefix+`.id = org_user.user_id`).
|
|
||||||
Cols(userprefix+`.*`, "org_user.role").
|
|
||||||
Where("org_user.org_id = ?", orgID).
|
|
||||||
Asc(userprefix + `.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
|
|
||||||
|
|
||||||
// Set an admin user with the
|
|
||||||
rowUser := &user.SignedInUser{
|
|
||||||
Login: "",
|
|
||||||
OrgID: orgID, // gets filled in from each row
|
|
||||||
UserID: 0,
|
|
||||||
}
|
|
||||||
ch.ctx = appcontext.WithUser(context.Background(), rowUser)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *commitHelper) add(opts commitOptions) error {
|
|
||||||
if ch.stopRequested {
|
|
||||||
return fmt.Errorf("stop requested")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(opts.body) < 1 {
|
|
||||||
return nil // nothing to commit
|
|
||||||
}
|
|
||||||
|
|
||||||
user, ok := ch.users[opts.userID]
|
|
||||||
if !ok {
|
|
||||||
user = &userInfo{
|
|
||||||
Name: "admin",
|
|
||||||
Email: "admin@unknown.org",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sig := user.getAuthor()
|
|
||||||
if opts.when.Unix() > 100 {
|
|
||||||
sig.When = opts.when
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = os.WriteFile(b.fpath, body, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = os.Chtimes(b.fpath, sig.When, sig.When)
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
ch.counter++
|
|
||||||
}
|
|
||||||
|
|
||||||
copts := &git.CommitOptions{
|
|
||||||
Author: &sig,
|
|
||||||
}
|
|
||||||
|
|
||||||
ch.broadcast(opts.body[0].fpath)
|
|
||||||
_, err := ch.work.Commit(opts.comment, copts)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type userInfo struct {
|
|
||||||
ID int64 `json:"-" db:"id"`
|
|
||||||
Login string `json:"login"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Salt string `json:"salt"`
|
|
||||||
Company string `json:"company,omitempty"`
|
|
||||||
Rands string `json:"-"`
|
|
||||||
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" db:"is_disabled"`
|
|
||||||
IsServiceAccount bool `json:"serviceAccount" db:"is_service_account"`
|
|
||||||
LastSeenAt time.Time `json:"-" db:"last_seen_at"`
|
|
||||||
|
|
||||||
// Added to make sqlx happy
|
|
||||||
Version int `json:"-"`
|
|
||||||
HelpFlags1 int `json:"-" db:"help_flags1"`
|
|
||||||
OrgID int64 `json:"-" db:"org_id"`
|
|
||||||
EmailVerified bool `json:"-" db:"email_verified"`
|
|
||||||
IsAdmin bool `json:"-" db:"is_admin"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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, "?"),
|
|
||||||
When: time.Now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstRealStringX(vals ...string) string {
|
|
||||||
for _, v := range vals {
|
|
||||||
if v != "" {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "?"
|
|
||||||
}
|
|
@ -1,105 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ Job = new(dummyExportJob)
|
|
||||||
|
|
||||||
type dummyExportJob struct {
|
|
||||||
logger log.Logger
|
|
||||||
|
|
||||||
statusMu sync.Mutex
|
|
||||||
status ExportStatus
|
|
||||||
cfg ExportConfig
|
|
||||||
broadcaster statusBroadcaster
|
|
||||||
stopRequested bool
|
|
||||||
total int
|
|
||||||
}
|
|
||||||
|
|
||||||
func startDummyExportJob(cfg ExportConfig, broadcaster statusBroadcaster) (Job, error) {
|
|
||||||
job := &dummyExportJob{
|
|
||||||
logger: log.New("dummy_export_job"),
|
|
||||||
cfg: cfg,
|
|
||||||
broadcaster: broadcaster,
|
|
||||||
status: ExportStatus{
|
|
||||||
Running: true,
|
|
||||||
Target: "dummy export",
|
|
||||||
Started: time.Now().UnixMilli(),
|
|
||||||
Count: make(map[string]int, 10),
|
|
||||||
Index: 0,
|
|
||||||
},
|
|
||||||
total: int(math.Round(10 + rand.Float64()*20)),
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcaster(job.status)
|
|
||||||
go job.start()
|
|
||||||
return job, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *dummyExportJob) requestStop() {
|
|
||||||
e.stopRequested = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *dummyExportJob) start() {
|
|
||||||
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")
|
|
||||||
|
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
|
||||||
for t := range ticker.C {
|
|
||||||
e.statusMu.Lock()
|
|
||||||
e.status.Changed = t.UnixMilli()
|
|
||||||
e.status.Index++
|
|
||||||
e.status.Last = fmt.Sprintf("ITEM: %d", e.status.Index)
|
|
||||||
e.statusMu.Unlock()
|
|
||||||
|
|
||||||
// Wait till we are done
|
|
||||||
shouldStop := e.stopRequested || e.status.Index >= e.total
|
|
||||||
e.broadcaster(e.status)
|
|
||||||
|
|
||||||
if shouldStop {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *dummyExportJob) getStatus() ExportStatus {
|
|
||||||
e.statusMu.Lock()
|
|
||||||
defer e.statusMu.Unlock()
|
|
||||||
|
|
||||||
return e.status
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *dummyExportJob) getConfig() ExportConfig {
|
|
||||||
e.statusMu.Lock()
|
|
||||||
defer e.statusMu.Unlock()
|
|
||||||
|
|
||||||
return e.cfg
|
|
||||||
}
|
|
@ -1,374 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/appcontext"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
|
||||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
|
||||||
"github.com/grafana/grafana/pkg/services/playlist"
|
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/session"
|
|
||||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
|
||||||
"github.com/grafana/grafana/pkg/services/store/kind/folder"
|
|
||||||
"github.com/grafana/grafana/pkg/services/store/kind/snapshot"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ Job = new(entityStoreJob)
|
|
||||||
|
|
||||||
type entityStoreJob struct {
|
|
||||||
logger log.Logger
|
|
||||||
|
|
||||||
statusMu sync.Mutex
|
|
||||||
status ExportStatus
|
|
||||||
cfg ExportConfig
|
|
||||||
broadcaster statusBroadcaster
|
|
||||||
stopRequested bool
|
|
||||||
ctx context.Context
|
|
||||||
|
|
||||||
sess *session.SessionDB
|
|
||||||
playlistService playlist.Service
|
|
||||||
store entity.EntityStoreServer
|
|
||||||
dashboardsnapshots dashboardsnapshots.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
func startEntityStoreJob(ctx context.Context,
|
|
||||||
cfg ExportConfig,
|
|
||||||
broadcaster statusBroadcaster,
|
|
||||||
db db.DB,
|
|
||||||
playlistService playlist.Service,
|
|
||||||
store entity.EntityStoreServer,
|
|
||||||
dashboardsnapshots dashboardsnapshots.Service,
|
|
||||||
) (Job, error) {
|
|
||||||
job := &entityStoreJob{
|
|
||||||
logger: log.New("export_to_object_store_job"),
|
|
||||||
cfg: cfg,
|
|
||||||
ctx: ctx,
|
|
||||||
broadcaster: broadcaster,
|
|
||||||
status: ExportStatus{
|
|
||||||
Running: true,
|
|
||||||
Target: "object store export",
|
|
||||||
Started: time.Now().UnixMilli(),
|
|
||||||
Count: make(map[string]int, 10),
|
|
||||||
Index: 0,
|
|
||||||
},
|
|
||||||
sess: db.GetSqlxSession(),
|
|
||||||
playlistService: playlistService,
|
|
||||||
store: store,
|
|
||||||
dashboardsnapshots: dashboardsnapshots,
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcaster(job.status)
|
|
||||||
go job.start(ctx)
|
|
||||||
return job, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *entityStoreJob) requestStop() {
|
|
||||||
e.stopRequested = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *entityStoreJob) start(ctx context.Context) {
|
|
||||||
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{
|
|
||||||
Login: "",
|
|
||||||
OrgID: 0, // gets filled in from each row
|
|
||||||
UserID: 0,
|
|
||||||
}
|
|
||||||
ctx = appcontext.WithUser(ctx, rowUser)
|
|
||||||
|
|
||||||
what := entity.StandardKindFolder
|
|
||||||
e.status.Count[what] = 0
|
|
||||||
|
|
||||||
folders := make(map[int64]string)
|
|
||||||
folderInfo, err := e.getFolders(ctx)
|
|
||||||
if err != nil {
|
|
||||||
e.status.Status = "error: " + err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.status.Last = fmt.Sprintf("export %d folders", len(folderInfo))
|
|
||||||
e.broadcaster(e.status)
|
|
||||||
|
|
||||||
for _, dash := range folderInfo {
|
|
||||||
folders[dash.ID] = dash.UID
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dash := range folderInfo {
|
|
||||||
rowUser.OrgID = dash.OrgID
|
|
||||||
rowUser.UserID = dash.UpdatedBy
|
|
||||||
if dash.UpdatedBy < 0 {
|
|
||||||
rowUser.UserID = 0 // avoid Uint64Val issue????
|
|
||||||
}
|
|
||||||
f := folder.Model{Name: dash.Title}
|
|
||||||
d, _ := json.Marshal(f)
|
|
||||||
|
|
||||||
_, err = e.store.AdminWrite(ctx, &entity.AdminWriteEntityRequest{
|
|
||||||
GRN: &entity.GRN{
|
|
||||||
UID: dash.UID,
|
|
||||||
Kind: entity.StandardKindFolder,
|
|
||||||
},
|
|
||||||
ClearHistory: true,
|
|
||||||
CreatedAt: dash.Created.UnixMilli(),
|
|
||||||
UpdatedAt: dash.Updated.UnixMilli(),
|
|
||||||
UpdatedBy: fmt.Sprintf("user:%d", dash.UpdatedBy),
|
|
||||||
CreatedBy: fmt.Sprintf("user:%d", dash.CreatedBy),
|
|
||||||
Body: d,
|
|
||||||
Folder: folders[dash.FolderID],
|
|
||||||
Comment: "(exported from SQL)",
|
|
||||||
Origin: &entity.EntityOriginInfo{
|
|
||||||
Source: "export-from-sql",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
what = entity.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
|
|
||||||
}
|
|
||||||
e.status.Last = fmt.Sprintf("export %d dashboards", len(dashInfo))
|
|
||||||
e.broadcaster(e.status)
|
|
||||||
|
|
||||||
for _, dash := range dashInfo {
|
|
||||||
rowUser.OrgID = dash.OrgID
|
|
||||||
rowUser.UserID = dash.UpdatedBy
|
|
||||||
if dash.UpdatedBy < 0 {
|
|
||||||
rowUser.UserID = 0 // avoid Uint64Val issue????
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = e.store.AdminWrite(ctx, &entity.AdminWriteEntityRequest{
|
|
||||||
GRN: &entity.GRN{
|
|
||||||
UID: dash.UID,
|
|
||||||
Kind: entity.StandardKindDashboard,
|
|
||||||
},
|
|
||||||
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),
|
|
||||||
Body: dash.Data,
|
|
||||||
Folder: folders[dash.FolderID],
|
|
||||||
Comment: "(exported from SQL)",
|
|
||||||
Origin: &entity.EntityOriginInfo{
|
|
||||||
Source: "export-from-sql",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
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 = entity.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, &entity.WriteEntityRequest{
|
|
||||||
GRN: &entity.GRN{
|
|
||||||
UID: playlist.Uid,
|
|
||||||
Kind: entity.StandardKindPlaylist,
|
|
||||||
},
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO.. query lookup
|
|
||||||
orgIDs := []int64{1}
|
|
||||||
what = "snapshot"
|
|
||||||
for _, orgId := range orgIDs {
|
|
||||||
rowUser.OrgID = orgId
|
|
||||||
rowUser.UserID = 1
|
|
||||||
cmd := &dashboardsnapshots.GetDashboardSnapshotsQuery{
|
|
||||||
OrgID: orgId,
|
|
||||||
Limit: 500000,
|
|
||||||
SignedInUser: rowUser,
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := e.dashboardsnapshots.SearchDashboardSnapshots(ctx, cmd)
|
|
||||||
if err != nil {
|
|
||||||
e.status.Status = "error: " + err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dto := range 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,
|
|
||||||
}
|
|
||||||
snapcmdResult, err := e.dashboardsnapshots.GetDashboardSnapshot(ctx, snapcmd)
|
|
||||||
if err == nil {
|
|
||||||
res := snapcmdResult
|
|
||||||
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, &entity.WriteEntityRequest{
|
|
||||||
GRN: &entity.GRN{
|
|
||||||
UID: dto.Key,
|
|
||||||
Kind: entity.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type dashInfo struct {
|
|
||||||
OrgID int64 `db:"org_id"`
|
|
||||||
UID string
|
|
||||||
Version int64
|
|
||||||
Slug string
|
|
||||||
Data []byte
|
|
||||||
Created time.Time
|
|
||||||
Updated time.Time
|
|
||||||
CreatedBy int64 `db:"created_by"`
|
|
||||||
UpdatedBy int64 `db:"updated_by"`
|
|
||||||
FolderID int64 `db:"folder_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type folderInfo struct {
|
|
||||||
ID int64 `db:"id"`
|
|
||||||
OrgID int64 `db:"org_id"`
|
|
||||||
UID string
|
|
||||||
Title string
|
|
||||||
Created time.Time
|
|
||||||
Updated time.Time
|
|
||||||
CreatedBy int64 `db:"created_by"`
|
|
||||||
UpdatedBy int64 `db:"updated_by"`
|
|
||||||
FolderID int64 `db:"folder_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO, paging etc
|
|
||||||
func (e *entityStoreJob) getDashboards(ctx context.Context) ([]dashInfo, error) {
|
|
||||||
e.status.Last = "find dashbaords...."
|
|
||||||
e.broadcaster(e.status)
|
|
||||||
|
|
||||||
dash := make([]dashInfo, 0)
|
|
||||||
err := e.sess.Select(ctx, &dash, "SELECT org_id,uid,version,slug,data,folder_id,created,updated,created_by,updated_by FROM dashboard WHERE is_folder=false")
|
|
||||||
return dash, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO, paging etc
|
|
||||||
func (e *entityStoreJob) getFolders(ctx context.Context) ([]folderInfo, error) {
|
|
||||||
e.status.Last = "find dashbaords...."
|
|
||||||
e.broadcaster(e.status)
|
|
||||||
|
|
||||||
dash := make([]folderInfo, 0)
|
|
||||||
err := e.sess.Select(ctx, &dash, "SELECT id,org_id,uid,title,folder_id,created,updated,created_by,updated_by FROM dashboard WHERE is_folder=true")
|
|
||||||
return dash, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *entityStoreJob) getStatus() ExportStatus {
|
|
||||||
e.statusMu.Lock()
|
|
||||||
defer e.statusMu.Unlock()
|
|
||||||
|
|
||||||
return e.status
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *entityStoreJob) getConfig() ExportConfig {
|
|
||||||
e.statusMu.Lock()
|
|
||||||
defer e.statusMu.Unlock()
|
|
||||||
|
|
||||||
return e.cfg
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportAlerts(helper *commitHelper, job *gitExportJob) error {
|
|
||||||
alertDir := path.Join(helper.orgDir, "alerts")
|
|
||||||
|
|
||||||
return job.sql.WithDbSession(helper.ctx, func(sess *db.Session) error {
|
|
||||||
type ruleResult struct {
|
|
||||||
Title string `xorm:"title"`
|
|
||||||
UID string `xorm:"uid"`
|
|
||||||
NamespaceUID string `xorm:"namespace_uid"`
|
|
||||||
RuleGroup string `xorm:"rule_group"`
|
|
||||||
Condition json.RawMessage `xorm:"data"`
|
|
||||||
DashboardUID string `xorm:"dashboard_uid"`
|
|
||||||
PanelID int64 `xorm:"panel_id"`
|
|
||||||
Updated time.Time `xorm:"updated" json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
rows := make([]*ruleResult, 0)
|
|
||||||
|
|
||||||
sess.Table("alert_rule").Where("org_id = ?", helper.orgID)
|
|
||||||
|
|
||||||
err := sess.Find(&rows)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range rows {
|
|
||||||
err = helper.add(commitOptions{
|
|
||||||
body: []commitBody{{
|
|
||||||
body: prettyJSON(row),
|
|
||||||
fpath: path.Join(alertDir, row.UID) + ".json", // must be JSON files
|
|
||||||
}},
|
|
||||||
comment: fmt.Sprintf("Alert: %s", row.Title),
|
|
||||||
when: row.Updated,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportAnnotations(helper *commitHelper, job *gitExportJob) error {
|
|
||||||
return job.sql.WithDbSession(helper.ctx, func(sess *db.Session) 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if f_ID.Len() > 0 {
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,138 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
func dumpAuthTables(helper *commitHelper, job *gitExportJob) error {
|
|
||||||
isMySQL := isMySQLEngine(job.sql)
|
|
||||||
|
|
||||||
return job.sql.WithDbSession(helper.ctx, func(sess *db.Session) error {
|
|
||||||
commit := commitOptions{
|
|
||||||
comment: "auth tables dump",
|
|
||||||
}
|
|
||||||
|
|
||||||
type statsTables struct {
|
|
||||||
table string
|
|
||||||
sql string
|
|
||||||
converters []sqlutil.Converter
|
|
||||||
drop []string
|
|
||||||
}
|
|
||||||
|
|
||||||
dump := []statsTables{
|
|
||||||
{
|
|
||||||
table: "user",
|
|
||||||
sql: removeQuotesFromQuery(`
|
|
||||||
SELECT "user".*, org_user.role
|
|
||||||
FROM "user"
|
|
||||||
JOIN org_user ON "user".id = org_user.user_id
|
|
||||||
WHERE org_user.org_id =`+strconv.FormatInt(helper.orgID, 10), isMySQL),
|
|
||||||
converters: []sqlutil.Converter{{Dynamic: true}},
|
|
||||||
drop: []string{
|
|
||||||
"id", "version",
|
|
||||||
"password", // UMMMMM... for now
|
|
||||||
"org_id",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
table: "user_role",
|
|
||||||
sql: `
|
|
||||||
SELECT * FROM user_role
|
|
||||||
WHERE org_id =` + strconv.FormatInt(helper.orgID, 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
table: "builtin_role",
|
|
||||||
sql: `
|
|
||||||
SELECT * FROM builtin_role
|
|
||||||
WHERE org_id =` + strconv.FormatInt(helper.orgID, 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
table: "api_key",
|
|
||||||
sql: `
|
|
||||||
SELECT * FROM api_key
|
|
||||||
WHERE org_id =` + strconv.FormatInt(helper.orgID, 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
table: "permission",
|
|
||||||
sql: `
|
|
||||||
SELECT permission.*
|
|
||||||
FROM permission
|
|
||||||
JOIN role ON permission.role_id = role.id
|
|
||||||
WHERE org_id =` + strconv.FormatInt(helper.orgID, 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
table: "user_auth_token",
|
|
||||||
sql: `
|
|
||||||
SELECT user_auth_token.*
|
|
||||||
FROM user_auth_token
|
|
||||||
JOIN org_user ON user_auth_token.id = org_user.user_id
|
|
||||||
WHERE org_user.org_id =` + strconv.FormatInt(helper.orgID, 10),
|
|
||||||
},
|
|
||||||
{table: "team"},
|
|
||||||
{table: "team_role"},
|
|
||||||
{table: "team_member"},
|
|
||||||
{table: "temp_user"},
|
|
||||||
{table: "role"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, auth := range dump {
|
|
||||||
if auth.sql == "" {
|
|
||||||
auth.sql = `
|
|
||||||
SELECT * FROM ` + auth.table + `
|
|
||||||
WHERE org_id =` + strconv.FormatInt(helper.orgID, 10)
|
|
||||||
}
|
|
||||||
if auth.converters == nil {
|
|
||||||
auth.converters = []sqlutil.Converter{{Dynamic: true}}
|
|
||||||
}
|
|
||||||
if auth.drop == nil {
|
|
||||||
auth.drop = []string{
|
|
||||||
"id",
|
|
||||||
"org_id",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := sess.DB().QueryContext(helper.ctx, auth.sql)
|
|
||||||
if err != nil {
|
|
||||||
if isTableNotExistsError(err) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
frame, err := sqlutil.FrameFromRows(rows.Rows, -1, auth.converters...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if frame.Fields[0].Len() < 1 {
|
|
||||||
continue // do not write empty structures
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(auth.drop) > 0 {
|
|
||||||
lookup := make(map[string]bool, len(auth.drop))
|
|
||||||
for _, v := range auth.drop {
|
|
||||||
lookup[v] = true
|
|
||||||
}
|
|
||||||
fields := make([]*data.Field, 0, len(frame.Fields))
|
|
||||||
for _, f := range frame.Fields {
|
|
||||||
if lookup[f.Name] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fields = append(fields, f)
|
|
||||||
}
|
|
||||||
frame.Fields = fields
|
|
||||||
}
|
|
||||||
frame.Name = auth.table
|
|
||||||
commit.body = append(commit.body, commitBody{
|
|
||||||
fpath: path.Join(helper.orgDir, "auth", "sql.dump", auth.table+".json"),
|
|
||||||
frame: frame,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return helper.add(commit)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,241 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/filestorage"
|
|
||||||
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportDashboards(helper *commitHelper, job *gitExportJob) 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 len(job.cfg.GeneralFolderPath) > 0 {
|
|
||||||
folders[0] = job.cfg.GeneralFolderPath // "general"
|
|
||||||
}
|
|
||||||
|
|
||||||
lookup, err := dashboard.LoadDatasourceLookup(helper.ctx, helper.orgID, job.sql)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rootDir := path.Join(helper.orgDir, "drive")
|
|
||||||
folderStructure := commitOptions{
|
|
||||||
when: time.Now(),
|
|
||||||
comment: "Exported folder structure",
|
|
||||||
}
|
|
||||||
|
|
||||||
err = job.sql.WithDbSession(helper.ctx, func(sess *db.Session) 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
|
|
||||||
}
|
|
||||||
|
|
||||||
reader := dashboard.NewStaticDashboardSummaryBuilder(lookup, false)
|
|
||||||
|
|
||||||
// Process all folders
|
|
||||||
for _, row := range rows {
|
|
||||||
if !row.IsFolder {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dash, _, err := reader(helper.ctx, row.UID, row.Data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dash.UID = row.UID
|
|
||||||
slug := cleanFileName(dash.Name)
|
|
||||||
folder := map[string]string{
|
|
||||||
"title": dash.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 + "-dashboard.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 *db.Session) error {
|
|
||||||
type dashVersionResult struct {
|
|
||||||
DashId int64 `xorm:"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))
|
|
||||||
|
|
||||||
if job.cfg.KeepHistory {
|
|
||||||
sess.Table("dashboard_version").
|
|
||||||
Join("INNER", "dashboard", "dashboard.id = dashboard_version.dashboard_id").
|
|
||||||
Where("org_id = ?", helper.orgID).
|
|
||||||
Cols("dashboard.id",
|
|
||||||
"dashboard_version.version",
|
|
||||||
"dashboard_version.created",
|
|
||||||
"dashboard_version.created_by",
|
|
||||||
"dashboard_version.message",
|
|
||||||
"dashboard_version.data").
|
|
||||||
Asc("dashboard_version.created")
|
|
||||||
} else {
|
|
||||||
sess.Table("dashboard").
|
|
||||||
Where("org_id = ?", helper.orgID).
|
|
||||||
Cols("id",
|
|
||||||
"version",
|
|
||||||
"created",
|
|
||||||
"created_by",
|
|
||||||
"data").
|
|
||||||
Asc("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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportDashboardThumbnails(helper *commitHelper, job *gitExportJob) error {
|
|
||||||
alias := make(map[string]string, 100)
|
|
||||||
aliasLookup, err := os.ReadFile(filepath.Join(helper.orgDir, "root-alias.json"))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("missing dashboard alias files (must export dashboards first)")
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(aliasLookup, &alias)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return job.sql.WithDbSession(helper.ctx, func(sess *db.Session) error {
|
|
||||||
type dashboardThumb struct {
|
|
||||||
UID string `xorm:"uid"`
|
|
||||||
Image []byte `xorm:"image"`
|
|
||||||
Theme string `xorm:"theme"`
|
|
||||||
Kind string `xorm:"kind"`
|
|
||||||
MimeType string `xorm:"mime_type"`
|
|
||||||
Updated time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
rows := make([]*dashboardThumb, 0)
|
|
||||||
|
|
||||||
// SELECT uid,image,theme,kind,mime_type,dashboard_thumbnail.updated
|
|
||||||
// FROM dashboard_thumbnail
|
|
||||||
// JOIN dashboard ON dashboard.id = dashboard_thumbnail.dashboard_id
|
|
||||||
// WHERE org_id = 2; //dashboard.uid = '2VVbg06nz';
|
|
||||||
|
|
||||||
sess.Table("dashboard_thumbnail").
|
|
||||||
Join("INNER", "dashboard", "dashboard.id = dashboard_thumbnail.dashboard_id").
|
|
||||||
Cols("uid", "image", "theme", "kind", "mime_type", "dashboard_thumbnail.updated").
|
|
||||||
Where("dashboard.org_id = ?", helper.orgID)
|
|
||||||
|
|
||||||
err := sess.Find(&rows)
|
|
||||||
if err != nil {
|
|
||||||
if isTableNotExistsError(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process all folders
|
|
||||||
for _, row := range rows {
|
|
||||||
p, ok := alias[row.UID]
|
|
||||||
if !ok {
|
|
||||||
p = "uid/" + row.UID
|
|
||||||
} else {
|
|
||||||
p = strings.TrimSuffix(p, "-dash.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := helper.add(commitOptions{
|
|
||||||
body: []commitBody{
|
|
||||||
{
|
|
||||||
fpath: filepath.Join(helper.orgDir, "thumbs", fmt.Sprintf("%s.thumb-%s.png", p, row.Theme)),
|
|
||||||
body: row.Image,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
when: row.Updated,
|
|
||||||
comment: "Thumbnail",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportDataSources(helper *commitHelper, job *gitExportJob) error {
|
|
||||||
cmd := &datasources.GetDataSourcesQuery{
|
|
||||||
OrgID: helper.orgID,
|
|
||||||
}
|
|
||||||
dataSources, err := job.datasourceService.GetDataSources(helper.ctx, cmd)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.SliceStable(dataSources, func(i, j int) bool {
|
|
||||||
return dataSources[i].Created.After(dataSources[j].Created)
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, ds := range dataSources {
|
|
||||||
ds.OrgID = 0
|
|
||||||
ds.Version = 0
|
|
||||||
ds.SecureJsonData = map[string][]byte{
|
|
||||||
"TODO": []byte("XXX"),
|
|
||||||
}
|
|
||||||
|
|
||||||
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 err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/filestorage"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportFiles(helper *commitHelper, job *gitExportJob) error {
|
|
||||||
fs := filestorage.NewDbStorage(log.New("grafanaStorageLogger"), job.sql, nil, fmt.Sprintf("/%d/", helper.orgID))
|
|
||||||
|
|
||||||
paging := &filestorage.Paging{}
|
|
||||||
for {
|
|
||||||
rsp, err := fs.List(helper.ctx, "/resources", paging, &filestorage.ListOptions{
|
|
||||||
WithFolders: false, // ????
|
|
||||||
Recursive: true,
|
|
||||||
WithContents: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range rsp.Files {
|
|
||||||
if f.Size < 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err = helper.add(commitOptions{
|
|
||||||
body: []commitBody{{
|
|
||||||
body: f.Contents,
|
|
||||||
fpath: path.Join(helper.orgDir, f.FullPath),
|
|
||||||
}},
|
|
||||||
comment: fmt.Sprintf("Adding: %s", path.Base(f.FullPath)),
|
|
||||||
when: f.Created,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
paging.After = rsp.LastPath
|
|
||||||
if !rsp.HasMore {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportKVStore(helper *commitHelper, job *gitExportJob) error {
|
|
||||||
kvdir := path.Join(helper.orgDir, "system", "kv_store")
|
|
||||||
|
|
||||||
return job.sql.WithDbSession(helper.ctx, func(sess *db.Session) error {
|
|
||||||
type kvResult struct {
|
|
||||||
Namespace string `xorm:"namespace"`
|
|
||||||
Key string `xorm:"key"`
|
|
||||||
Value string `xorm:"value"`
|
|
||||||
Updated time.Time `xorm:"updated"`
|
|
||||||
}
|
|
||||||
|
|
||||||
rows := make([]*kvResult, 0)
|
|
||||||
|
|
||||||
sess.Table("kv_store").Where("org_id = ? OR org_id = 0", helper.orgID)
|
|
||||||
|
|
||||||
err := sess.Find(&rows)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range rows {
|
|
||||||
err = helper.add(commitOptions{
|
|
||||||
body: []commitBody{{
|
|
||||||
body: []byte(row.Value),
|
|
||||||
fpath: path.Join(kvdir, row.Namespace, row.Key),
|
|
||||||
}},
|
|
||||||
comment: fmt.Sprintf("Exporting: %s/%s", row.Namespace, row.Key),
|
|
||||||
when: row.Updated,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportPlugins(helper *commitHelper, job *gitExportJob) error {
|
|
||||||
return job.sql.WithDbSession(helper.ctx, func(sess *db.Session) error {
|
|
||||||
type pResult struct {
|
|
||||||
PluginID string `xorm:"plugin_id" json:"-"`
|
|
||||||
Enabled string `xorm:"enabled" json:"enabled"`
|
|
||||||
Pinned string `xorm:"pinned" json:"pinned"`
|
|
||||||
JSONData json.RawMessage `xorm:"json_data" json:"json_data,omitempty"`
|
|
||||||
// TODO: secure!!!!
|
|
||||||
PluginVersion string `xorm:"plugin_version" json:"version"`
|
|
||||||
Created time.Time `xorm:"created" json:"created"`
|
|
||||||
Updated time.Time `xorm:"updated" json:"updated"`
|
|
||||||
}
|
|
||||||
|
|
||||||
rows := make([]*pResult, 0)
|
|
||||||
|
|
||||||
sess.Table("plugin_setting").Where("org_id = ?", helper.orgID)
|
|
||||||
|
|
||||||
err := sess.Find(&rows)
|
|
||||||
if err != nil {
|
|
||||||
if isTableNotExistsError(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range rows {
|
|
||||||
err = helper.add(commitOptions{
|
|
||||||
body: []commitBody{{
|
|
||||||
body: prettyJSON(row),
|
|
||||||
fpath: path.Join(helper.orgDir, "plugins", row.PluginID, "settings.json"),
|
|
||||||
}},
|
|
||||||
comment: fmt.Sprintf("Plugin: %s", row.PluginID),
|
|
||||||
when: row.Updated,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
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: helper.orgID,
|
|
||||||
Limit: 500000,
|
|
||||||
SignedInUser: nil,
|
|
||||||
}
|
|
||||||
if cmd.SignedInUser == nil {
|
|
||||||
return fmt.Errorf("snapshots requires an admin user")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := job.dashboardsnapshotsService.SearchDashboardSnapshots(helper.ctx, cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(result) < 1 {
|
|
||||||
return nil // nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
gitcmd := commitOptions{
|
|
||||||
when: time.Now(),
|
|
||||||
comment: "Export snapshots",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, snapshot := range 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)
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/playlist"
|
|
||||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportSystemPlaylists(helper *commitHelper, job *gitExportJob) error {
|
|
||||||
cmd := &playlist.GetPlaylistsQuery{
|
|
||||||
OrgId: helper.orgID,
|
|
||||||
Limit: 500000,
|
|
||||||
}
|
|
||||||
res, err := job.playlistService.Search(helper.ctx, cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(res) < 1 {
|
|
||||||
return nil // nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
gitcmd := commitOptions{
|
|
||||||
when: time.Now(),
|
|
||||||
comment: "Export playlists",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range res {
|
|
||||||
playlist, err := job.playlistService.Get(helper.ctx, &playlist.GetPlaylistByUidQuery{
|
|
||||||
UID: item.UID,
|
|
||||||
OrgId: helper.orgID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
gitcmd.body = append(gitcmd.body, commitBody{
|
|
||||||
fpath: filepath.Join(
|
|
||||||
helper.orgDir,
|
|
||||||
"entity",
|
|
||||||
entity.StandardKindPlaylist,
|
|
||||||
fmt.Sprintf("%s.json", playlist.Uid)),
|
|
||||||
body: prettyJSON(playlist),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return helper.add(gitcmd)
|
|
||||||
}
|
|
@ -1,136 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 *db.Session) 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)
|
|
||||||
if user.IsServiceAccount {
|
|
||||||
continue // don't write preferences for service account
|
|
||||||
}
|
|
||||||
} 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 {
|
|
||||||
if user.IsServiceAccount {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportSystemShortURL(helper *commitHelper, job *gitExportJob) error {
|
|
||||||
mostRecent := int64(0)
|
|
||||||
lastSeen := make(map[string]int64, 50)
|
|
||||||
dir := filepath.Join(helper.orgDir, "system", "short_url")
|
|
||||||
|
|
||||||
err := job.sql.WithDbSession(helper.ctx, func(sess *db.Session) error {
|
|
||||||
type urlResult struct {
|
|
||||||
UID string `xorm:"uid" json:"-"`
|
|
||||||
Path string `xorm:"path" json:"path"`
|
|
||||||
CreatedBy int64 `xorm:"created_by" json:"-"`
|
|
||||||
CreatedAt time.Time `xorm:"created_at" json:"-"`
|
|
||||||
LastSeenAt int64 `xorm:"last_seen_at" json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
rows := make([]*urlResult, 0)
|
|
||||||
|
|
||||||
sess.Table("short_url").Where("org_id = ?", helper.orgID)
|
|
||||||
|
|
||||||
err := sess.Find(&rows)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range rows {
|
|
||||||
if row.LastSeenAt > 0 {
|
|
||||||
lastSeen[row.UID] = row.LastSeenAt
|
|
||||||
if mostRecent < row.LastSeenAt {
|
|
||||||
mostRecent = row.LastSeenAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err := helper.add(commitOptions{
|
|
||||||
body: []commitBody{
|
|
||||||
{
|
|
||||||
fpath: filepath.Join(dir, "uid", fmt.Sprintf("%s.json", row.UID)),
|
|
||||||
body: prettyJSON(row),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
when: row.CreatedAt,
|
|
||||||
comment: "short URL",
|
|
||||||
userID: row.CreatedBy,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
if err != nil || len(lastSeen) < 1 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return helper.add(commitOptions{
|
|
||||||
body: []commitBody{
|
|
||||||
{
|
|
||||||
fpath: filepath.Join(dir, "last_seen_at.json"),
|
|
||||||
body: prettyJSON(lastSeen),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
when: time.UnixMilli(mostRecent),
|
|
||||||
comment: "short URL",
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportSystemStars(helper *commitHelper, job *gitExportJob) error {
|
|
||||||
byUser := make(map[int64][]string, 50)
|
|
||||||
|
|
||||||
err := job.sql.WithDbSession(helper.ctx, func(sess *db.Session) 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
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
func exportUsage(helper *commitHelper, job *gitExportJob) error {
|
|
||||||
return job.sql.WithDbSession(helper.ctx, func(sess *db.Session) error {
|
|
||||||
commit := commitOptions{
|
|
||||||
comment: "usage stats",
|
|
||||||
}
|
|
||||||
|
|
||||||
type statsTables struct {
|
|
||||||
table string
|
|
||||||
sql string
|
|
||||||
converters []sqlutil.Converter
|
|
||||||
}
|
|
||||||
|
|
||||||
dump := []statsTables{
|
|
||||||
{
|
|
||||||
table: "data_source_usage_by_day",
|
|
||||||
sql: `SELECT day,uid,queries,errors,load_duration_ms
|
|
||||||
FROM data_source_usage_by_day
|
|
||||||
JOIN data_source ON data_source.id = data_source_usage_by_day.data_source_id
|
|
||||||
WHERE org_id =` + strconv.FormatInt(helper.orgID, 10),
|
|
||||||
converters: []sqlutil.Converter{{Dynamic: true}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
table: "dashboard_usage_by_day",
|
|
||||||
sql: `SELECT uid,day,views,queries,errors,load_duration
|
|
||||||
FROM dashboard_usage_by_day
|
|
||||||
JOIN dashboard ON dashboard_usage_by_day.dashboard_id = dashboard.id
|
|
||||||
WHERE org_id =` + strconv.FormatInt(helper.orgID, 10),
|
|
||||||
converters: []sqlutil.Converter{{Dynamic: true}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
table: "dashboard_usage_sums",
|
|
||||||
sql: `SELECT uid,
|
|
||||||
views_last_1_days,
|
|
||||||
views_last_7_days,
|
|
||||||
views_last_30_days,
|
|
||||||
views_total,
|
|
||||||
queries_last_1_days,
|
|
||||||
queries_last_7_days,
|
|
||||||
queries_last_30_days,
|
|
||||||
queries_total,
|
|
||||||
errors_last_1_days,
|
|
||||||
errors_last_7_days,
|
|
||||||
errors_last_30_days,
|
|
||||||
errors_total
|
|
||||||
FROM dashboard_usage_sums
|
|
||||||
JOIN dashboard ON dashboard_usage_sums.dashboard_id = dashboard.id
|
|
||||||
WHERE org_id =` + strconv.FormatInt(helper.orgID, 10),
|
|
||||||
converters: []sqlutil.Converter{{Dynamic: true}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, usage := range dump {
|
|
||||||
rows, err := sess.DB().QueryContext(helper.ctx, usage.sql)
|
|
||||||
if err != nil {
|
|
||||||
if isTableNotExistsError(err) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
frame, err := sqlutil.FrameFromRows(rows.Rows, -1, usage.converters...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
frame.Name = usage.table
|
|
||||||
commit.body = append(commit.body, commitBody{
|
|
||||||
fpath: path.Join(helper.orgDir, "usage", usage.table+".json"),
|
|
||||||
frame: frame,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return helper.add(commit)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,233 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"runtime/debug"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
|
||||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
|
||||||
"github.com/grafana/grafana/pkg/services/playlist"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ Job = new(gitExportJob)
|
|
||||||
|
|
||||||
type gitExportJob struct {
|
|
||||||
logger log.Logger
|
|
||||||
sql db.DB
|
|
||||||
dashboardsnapshotsService dashboardsnapshots.Service
|
|
||||||
datasourceService datasources.DataSourceService
|
|
||||||
playlistService playlist.Service
|
|
||||||
orgService org.Service
|
|
||||||
rootDir string
|
|
||||||
|
|
||||||
statusMu sync.Mutex
|
|
||||||
status ExportStatus
|
|
||||||
cfg ExportConfig
|
|
||||||
broadcaster statusBroadcaster
|
|
||||||
helper *commitHelper
|
|
||||||
}
|
|
||||||
|
|
||||||
func startGitExportJob(ctx context.Context, cfg ExportConfig, sql db.DB,
|
|
||||||
dashboardsnapshotsService dashboardsnapshots.Service, rootDir string, orgID int64,
|
|
||||||
broadcaster statusBroadcaster, playlistService playlist.Service, orgService org.Service,
|
|
||||||
datasourceService datasources.DataSourceService) (Job, error) {
|
|
||||||
job := &gitExportJob{
|
|
||||||
logger: log.New("git_export_job"),
|
|
||||||
cfg: cfg,
|
|
||||||
sql: sql,
|
|
||||||
dashboardsnapshotsService: dashboardsnapshotsService,
|
|
||||||
playlistService: playlistService,
|
|
||||||
orgService: orgService,
|
|
||||||
datasourceService: datasourceService,
|
|
||||||
rootDir: rootDir,
|
|
||||||
broadcaster: broadcaster,
|
|
||||||
status: ExportStatus{
|
|
||||||
Running: true,
|
|
||||||
Target: "git export",
|
|
||||||
Started: time.Now().UnixMilli(),
|
|
||||||
Count: make(map[string]int, len(exporters)*2),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcaster(job.status)
|
|
||||||
go job.start(ctx)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *gitExportJob) requestStop() {
|
|
||||||
e.helper.stopRequested = true // will error on the next write
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility function to export dashboards
|
|
||||||
func (e *gitExportJob) start(ctx context.Context) {
|
|
||||||
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)
|
|
||||||
e.logger.Error("trace", "error", string(debug.Stack()))
|
|
||||||
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(ctx)
|
|
||||||
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(ctx context.Context) 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
|
|
||||||
}
|
|
||||||
e.helper = &commitHelper{
|
|
||||||
repo: r,
|
|
||||||
work: w,
|
|
||||||
ctx: ctx,
|
|
||||||
workDir: e.rootDir,
|
|
||||||
orgDir: e.rootDir,
|
|
||||||
broadcast: func(p string) {
|
|
||||||
e.status.Index++
|
|
||||||
e.status.Last = p[len(e.rootDir):]
|
|
||||||
e.status.Changed = time.Now().UnixMilli()
|
|
||||||
e.broadcaster(e.status)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := &org.SearchOrgsQuery{}
|
|
||||||
result, err := e.orgService.Search(e.helper.ctx, cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export each org
|
|
||||||
for _, org := range result {
|
|
||||||
if len(result) > 1 {
|
|
||||||
e.helper.orgDir = path.Join(e.rootDir, fmt.Sprintf("org_%d", org.ID))
|
|
||||||
e.status.Count["orgs"] += 1
|
|
||||||
}
|
|
||||||
err = e.helper.initOrg(ctx, e.sql, org.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err := e.process(exporters)
|
|
||||||
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) process(exporters []Exporter) error {
|
|
||||||
if false { // NEEDS a real user ID first
|
|
||||||
err := exportSnapshots(e.helper, e)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, exp := range exporters {
|
|
||||||
if e.cfg.Exclude[exp.Key] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
e.status.Target = exp.Key
|
|
||||||
e.helper.exporter = exp.Key
|
|
||||||
|
|
||||||
before := e.helper.counter
|
|
||||||
if exp.process != nil {
|
|
||||||
err := exp.process(e.helper, e)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if exp.Exporters != nil {
|
|
||||||
err := e.process(exp.Exporters)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aggregate the counts for each org in the same report
|
|
||||||
e.status.Count[exp.Key] += (e.helper.counter - before)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func prettyJSON(v interface{}) []byte {
|
|
||||||
b, _ := json.MarshalIndent(v, "", " ")
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
|
|
||||||
git remote add origin git@github.com:ryantxu/test-dash-repo.git
|
|
||||||
git branch -M main
|
|
||||||
git push -u origin main
|
|
||||||
|
|
||||||
**/
|
|
@ -1,267 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/appcontext"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
|
||||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
|
||||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
||||||
"github.com/grafana/grafana/pkg/services/live"
|
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
|
||||||
"github.com/grafana/grafana/pkg/services/playlist"
|
|
||||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ExportService interface {
|
|
||||||
// List folder contents
|
|
||||||
HandleGetStatus(c *contextmodel.ReqContext) response.Response
|
|
||||||
|
|
||||||
// List Get Options
|
|
||||||
HandleGetOptions(c *contextmodel.ReqContext) response.Response
|
|
||||||
|
|
||||||
// Read raw file contents out of the store
|
|
||||||
HandleRequestExport(c *contextmodel.ReqContext) response.Response
|
|
||||||
|
|
||||||
// Cancel any running export
|
|
||||||
HandleRequestStop(c *contextmodel.ReqContext) response.Response
|
|
||||||
}
|
|
||||||
|
|
||||||
var exporters = []Exporter{
|
|
||||||
{
|
|
||||||
Key: "auth",
|
|
||||||
Name: "Authentication",
|
|
||||||
Description: "Saves raw SQL tables",
|
|
||||||
process: dumpAuthTables,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "dash",
|
|
||||||
Name: "Dashboards",
|
|
||||||
Description: "Save dashboard JSON",
|
|
||||||
process: exportDashboards,
|
|
||||||
Exporters: []Exporter{
|
|
||||||
{
|
|
||||||
Key: "dash_thumbs",
|
|
||||||
Name: "Dashboard thumbnails",
|
|
||||||
Description: "Save current dashboard preview images",
|
|
||||||
process: exportDashboardThumbnails,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "alerts",
|
|
||||||
Name: "Alerts",
|
|
||||||
Description: "Archive alert rules and configuration",
|
|
||||||
process: exportAlerts,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "ds",
|
|
||||||
Name: "Data sources",
|
|
||||||
Description: "Data source configurations",
|
|
||||||
process: exportDataSources,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "system",
|
|
||||||
Name: "System",
|
|
||||||
Description: "Save service settings",
|
|
||||||
Exporters: []Exporter{
|
|
||||||
{
|
|
||||||
Key: "system_preferences",
|
|
||||||
Name: "Preferences",
|
|
||||||
Description: "User and team preferences",
|
|
||||||
process: exportSystemPreferences,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "system_stars",
|
|
||||||
Name: "Stars",
|
|
||||||
Description: "User stars",
|
|
||||||
process: exportSystemStars,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "system_playlists",
|
|
||||||
Name: "Playlists",
|
|
||||||
Description: "Playlists",
|
|
||||||
process: exportSystemPlaylists,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "system_kv_store",
|
|
||||||
Name: "Key Value store",
|
|
||||||
Description: "Internal KV store",
|
|
||||||
process: exportKVStore,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "system_short_url",
|
|
||||||
Name: "Short URLs",
|
|
||||||
Description: "saved links",
|
|
||||||
process: exportSystemShortURL,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "files",
|
|
||||||
Name: "Files",
|
|
||||||
Description: "Export internal file system",
|
|
||||||
process: exportFiles,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "anno",
|
|
||||||
Name: "Annotations",
|
|
||||||
Description: "Write an DataFrame for all annotations on a dashboard",
|
|
||||||
process: exportAnnotations,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "plugins",
|
|
||||||
Name: "Plugins",
|
|
||||||
Description: "Save settings for all configured plugins",
|
|
||||||
process: exportPlugins,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "usage",
|
|
||||||
Name: "Usage",
|
|
||||||
Description: "archive current usage stats",
|
|
||||||
process: exportUsage,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// Key: "snapshots",
|
|
||||||
// Name: "Snapshots",
|
|
||||||
// Description: "write snapshots",
|
|
||||||
// process: exportSnapshots,
|
|
||||||
// },
|
|
||||||
}
|
|
||||||
|
|
||||||
type StandardExport struct {
|
|
||||||
logger log.Logger
|
|
||||||
glive *live.GrafanaLive
|
|
||||||
mutex sync.Mutex
|
|
||||||
dataDir string
|
|
||||||
|
|
||||||
// Services
|
|
||||||
db db.DB
|
|
||||||
dashboardsnapshotsService dashboardsnapshots.Service
|
|
||||||
playlistService playlist.Service
|
|
||||||
orgService org.Service
|
|
||||||
datasourceService datasources.DataSourceService
|
|
||||||
store entity.EntityStoreServer
|
|
||||||
|
|
||||||
// updated with mutex
|
|
||||||
exportJob Job
|
|
||||||
}
|
|
||||||
|
|
||||||
func ProvideService(db db.DB, features featuremgmt.FeatureToggles, gl *live.GrafanaLive, cfg *setting.Cfg,
|
|
||||||
dashboardsnapshotsService dashboardsnapshots.Service, playlistService playlist.Service, orgService org.Service,
|
|
||||||
datasourceService datasources.DataSourceService, store entity.EntityStoreServer) ExportService {
|
|
||||||
if !features.IsEnabled(featuremgmt.FlagExport) {
|
|
||||||
return &StubExport{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &StandardExport{
|
|
||||||
glive: gl,
|
|
||||||
logger: log.New("export_service"),
|
|
||||||
dashboardsnapshotsService: dashboardsnapshotsService,
|
|
||||||
playlistService: playlistService,
|
|
||||||
orgService: orgService,
|
|
||||||
datasourceService: datasourceService,
|
|
||||||
exportJob: &stoppedJob{},
|
|
||||||
dataDir: cfg.DataPath,
|
|
||||||
store: store,
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ex *StandardExport) HandleGetOptions(c *contextmodel.ReqContext) response.Response {
|
|
||||||
info := map[string]interface{}{
|
|
||||||
"exporters": exporters,
|
|
||||||
}
|
|
||||||
return response.JSON(http.StatusOK, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ex *StandardExport) HandleGetStatus(c *contextmodel.ReqContext) response.Response {
|
|
||||||
ex.mutex.Lock()
|
|
||||||
defer ex.mutex.Unlock()
|
|
||||||
|
|
||||||
return response.JSON(http.StatusOK, ex.exportJob.getStatus())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ex *StandardExport) HandleRequestStop(c *contextmodel.ReqContext) response.Response {
|
|
||||||
ex.mutex.Lock()
|
|
||||||
defer ex.mutex.Unlock()
|
|
||||||
|
|
||||||
ex.exportJob.requestStop()
|
|
||||||
|
|
||||||
return response.JSON(http.StatusOK, ex.exportJob.getStatus())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ex *StandardExport) HandleRequestExport(c *contextmodel.ReqContext) response.Response {
|
|
||||||
var cfg ExportConfig
|
|
||||||
err := json.NewDecoder(c.Req.Body).Decode(&cfg)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(http.StatusBadRequest, "unable to read config", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ex.mutex.Lock()
|
|
||||||
defer ex.mutex.Unlock()
|
|
||||||
|
|
||||||
status := ex.exportJob.getStatus()
|
|
||||||
if status.Running {
|
|
||||||
ex.logger.Error("export already running")
|
|
||||||
return response.Error(http.StatusLocked, "export already running", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := appcontext.WithUser(context.Background(), c.SignedInUser)
|
|
||||||
var job Job
|
|
||||||
broadcast := func(s ExportStatus) {
|
|
||||||
ex.broadcastStatus(c.OrgID, s)
|
|
||||||
}
|
|
||||||
switch cfg.Format {
|
|
||||||
case "dummy":
|
|
||||||
job, err = startDummyExportJob(cfg, broadcast)
|
|
||||||
case "entityStore":
|
|
||||||
job, err = startEntityStoreJob(ctx, cfg, broadcast, ex.db, ex.playlistService, ex.store, ex.dashboardsnapshotsService)
|
|
||||||
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(ctx, cfg, ex.db, ex.dashboardsnapshotsService, dir, c.OrgID, broadcast, ex.playlistService, ex.orgService, ex.datasourceService)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
ex.exportJob = job
|
|
||||||
|
|
||||||
info := map[string]interface{}{
|
|
||||||
"cfg": cfg, // parsed job we are running
|
|
||||||
"status": ex.exportJob.getStatus(),
|
|
||||||
}
|
|
||||||
return response.JSON(http.StatusOK, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ex *StandardExport) broadcastStatus(orgID int64, s ExportStatus) {
|
|
||||||
msg, err := json.Marshal(s)
|
|
||||||
if err != nil {
|
|
||||||
ex.logger.Warn("Error making message", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = ex.glive.Publish(orgID, "grafana/broadcast/export", msg)
|
|
||||||
if err != nil {
|
|
||||||
ex.logger.Warn("Error Publish message", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
var _ Job = new(stoppedJob)
|
|
||||||
|
|
||||||
type stoppedJob struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *stoppedJob) getStatus() ExportStatus {
|
|
||||||
return ExportStatus{
|
|
||||||
Running: false,
|
|
||||||
Changed: time.Now().UnixMilli(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *stoppedJob) getConfig() ExportConfig {
|
|
||||||
return ExportConfig{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *stoppedJob) requestStop() {}
|
|
@ -1,28 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
|
||||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ ExportService = new(StubExport)
|
|
||||||
|
|
||||||
type StubExport struct{}
|
|
||||||
|
|
||||||
func (ex *StubExport) HandleGetStatus(c *contextmodel.ReqContext) response.Response {
|
|
||||||
return response.Error(http.StatusForbidden, "feature not enabled", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ex *StubExport) HandleGetOptions(c *contextmodel.ReqContext) response.Response {
|
|
||||||
return response.Error(http.StatusForbidden, "feature not enabled", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ex *StubExport) HandleRequestExport(c *contextmodel.ReqContext) response.Response {
|
|
||||||
return response.Error(http.StatusForbidden, "feature not enabled", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ex *StubExport) HandleRequestStop(c *contextmodel.ReqContext) response.Response {
|
|
||||||
return response.Error(http.StatusForbidden, "feature not enabled", nil)
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
// Export status. Only one running at a time
|
|
||||||
type ExportStatus struct {
|
|
||||||
Running bool `json:"running"`
|
|
||||||
Target string `json:"target"` // description of where it is going (no secrets)
|
|
||||||
Started int64 `json:"started,omitempty"`
|
|
||||||
Finished int64 `json:"finished,omitempty"`
|
|
||||||
Changed int64 `json:"update,omitempty"`
|
|
||||||
Last string `json:"last,omitempty"`
|
|
||||||
Status string `json:"status"` // ERROR, SUCCESS, ETC
|
|
||||||
Index int `json:"index,omitempty"`
|
|
||||||
Count map[string]int `json:"count,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic export config (for now)
|
|
||||||
type ExportConfig struct {
|
|
||||||
Format string `json:"format"`
|
|
||||||
GeneralFolderPath string `json:"generalFolderPath"`
|
|
||||||
KeepHistory bool `json:"history"`
|
|
||||||
|
|
||||||
Exclude map[string]bool `json:"exclude"`
|
|
||||||
|
|
||||||
// Depends on the format
|
|
||||||
Git GitExportConfig `json:"git"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GitExportConfig struct{}
|
|
||||||
|
|
||||||
type Job interface {
|
|
||||||
getStatus() ExportStatus
|
|
||||||
getConfig() ExportConfig
|
|
||||||
requestStop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Will broadcast the live status
|
|
||||||
type statusBroadcaster func(s ExportStatus)
|
|
||||||
|
|
||||||
type Exporter struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Exporters []Exporter `json:"exporters,omitempty"`
|
|
||||||
|
|
||||||
process func(helper *commitHelper, job *gitExportJob) error
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package export
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
func isTableNotExistsError(err error) bool {
|
|
||||||
txt := err.Error()
|
|
||||||
return strings.HasPrefix(txt, "no such table") || // SQLite
|
|
||||||
strings.HasSuffix(txt, " does not exist") || // PostgreSQL
|
|
||||||
strings.HasSuffix(txt, " doesn't exist") // MySQL
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeQuotesFromQuery(query string, remove bool) string {
|
|
||||||
if remove {
|
|
||||||
return strings.ReplaceAll(query, `"`, "")
|
|
||||||
}
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
func isMySQLEngine(sql db.DB) bool {
|
|
||||||
return sql.GetDBType() == "mysql"
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPostgreSQL(sql db.DB) bool {
|
|
||||||
return sql.GetDBType() == "postgres"
|
|
||||||
}
|
|
@ -120,12 +120,6 @@ var (
|
|||||||
State: FeatureStateAlpha,
|
State: FeatureStateAlpha,
|
||||||
RequiresDevMode: true, // Also a gate on automatic git storage (for now)
|
RequiresDevMode: true, // Also a gate on automatic git storage (for now)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "export",
|
|
||||||
Description: "Export grafana instance (to git, etc)",
|
|
||||||
State: FeatureStateAlpha,
|
|
||||||
RequiresDevMode: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "exploreMixedDatasource",
|
Name: "exploreMixedDatasource",
|
||||||
Description: "Enable mixed datasource in Explore",
|
Description: "Enable mixed datasource in Explore",
|
||||||
|
@ -91,10 +91,6 @@ const (
|
|||||||
// Load dashboards from the generic storage interface
|
// Load dashboards from the generic storage interface
|
||||||
FlagDashboardsFromStorage = "dashboardsFromStorage"
|
FlagDashboardsFromStorage = "dashboardsFromStorage"
|
||||||
|
|
||||||
// FlagExport
|
|
||||||
// Export grafana instance (to git, etc)
|
|
||||||
FlagExport = "export"
|
|
||||||
|
|
||||||
// FlagExploreMixedDatasource
|
// FlagExploreMixedDatasource
|
||||||
// Enable mixed datasource in Explore
|
// Enable mixed datasource in Explore
|
||||||
FlagExploreMixedDatasource = "exploreMixedDatasource"
|
FlagExploreMixedDatasource = "exploreMixedDatasource"
|
||||||
|
@ -161,16 +161,6 @@ func (s *ServiceImpl) getServerAdminNode(c *contextmodel.ReqContext) *navtree.Na
|
|||||||
Url: s.cfg.AppSubURL + "/admin/storage",
|
Url: s.cfg.AppSubURL + "/admin/storage",
|
||||||
}
|
}
|
||||||
adminNavLinks = append(adminNavLinks, storage)
|
adminNavLinks = append(adminNavLinks, storage)
|
||||||
|
|
||||||
if s.features.IsEnabled(featuremgmt.FlagExport) {
|
|
||||||
storage.Children = append(storage.Children, &navtree.NavLink{
|
|
||||||
Text: "Export",
|
|
||||||
Id: "export",
|
|
||||||
SubTitle: "Export grafana settings",
|
|
||||||
Icon: "cube",
|
|
||||||
Url: s.cfg.AppSubURL + "/admin/storage/export",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.cfg.LDAPEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)) {
|
if s.cfg.LDAPEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)) {
|
||||||
|
@ -1,278 +0,0 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
|
||||||
import { useAsync, useLocalStorage } from 'react-use';
|
|
||||||
|
|
||||||
import { isLiveChannelMessageEvent, isLiveChannelStatusEvent, LiveChannelScope, SelectableValue } from '@grafana/data';
|
|
||||||
import { getBackendSrv, getGrafanaLiveSrv, config } from '@grafana/runtime';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
CodeEditor,
|
|
||||||
Collapse,
|
|
||||||
Field,
|
|
||||||
HorizontalGroup,
|
|
||||||
InlineField,
|
|
||||||
InlineFieldRow,
|
|
||||||
InlineSwitch,
|
|
||||||
Input,
|
|
||||||
LinkButton,
|
|
||||||
Select,
|
|
||||||
Switch,
|
|
||||||
Alert,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
|
||||||
import { useNavModel } from 'app/core/hooks/useNavModel';
|
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
|
||||||
|
|
||||||
export const EXPORT_LOCAL_STORAGE_KEY = 'grafana.export.config';
|
|
||||||
|
|
||||||
interface ExportStatusMessage {
|
|
||||||
running: boolean;
|
|
||||||
target: string;
|
|
||||||
started: number;
|
|
||||||
finished: number;
|
|
||||||
update: number;
|
|
||||||
count: number;
|
|
||||||
current: number;
|
|
||||||
last: string;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExportJob {
|
|
||||||
format: string; // 'git';
|
|
||||||
generalFolderPath: string;
|
|
||||||
history: boolean;
|
|
||||||
exclude: Record<string, boolean>;
|
|
||||||
|
|
||||||
git?: {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultJob: ExportJob = {
|
|
||||||
format: 'git',
|
|
||||||
generalFolderPath: 'general',
|
|
||||||
history: true,
|
|
||||||
exclude: {},
|
|
||||||
git: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ExporterInfo {
|
|
||||||
key: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
children?: ExporterInfo[];
|
|
||||||
}
|
|
||||||
|
|
||||||
enum StorageFormat {
|
|
||||||
Git = 'git',
|
|
||||||
EntityStore = 'entityStore',
|
|
||||||
}
|
|
||||||
|
|
||||||
const formats: Array<SelectableValue<string>> = [
|
|
||||||
{ label: 'GIT', value: StorageFormat.Git, description: 'Exports a fresh git repository' },
|
|
||||||
{ label: 'Entity store', value: StorageFormat.EntityStore, description: 'Export to the SQL based entity store' },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface Props extends GrafanaRouteComponentProps {}
|
|
||||||
|
|
||||||
const labelWith = 18;
|
|
||||||
|
|
||||||
export default function ExportPage(props: Props) {
|
|
||||||
const navModel = useNavModel('export');
|
|
||||||
const [status, setStatus] = useState<ExportStatusMessage>();
|
|
||||||
const [body, setBody] = useLocalStorage<ExportJob>(EXPORT_LOCAL_STORAGE_KEY, defaultJob);
|
|
||||||
const [details, setDetails] = useState(false);
|
|
||||||
|
|
||||||
const serverOptions = useAsync(() => {
|
|
||||||
return getBackendSrv().get<{ exporters: ExporterInfo[] }>('/api/admin/export/options');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const doStart = () => {
|
|
||||||
getBackendSrv()
|
|
||||||
.post('/api/admin/export', body)
|
|
||||||
.then((v) => {
|
|
||||||
if (v.cfg && v.status.running) {
|
|
||||||
setBody(v.cfg); // saves the valid parsed body
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const doStop = () => {
|
|
||||||
getBackendSrv().post('/api/admin/export/stop');
|
|
||||||
};
|
|
||||||
|
|
||||||
const setInclude = useCallback(
|
|
||||||
(k: string, v: boolean) => {
|
|
||||||
if (!serverOptions.value || !body) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const exclude: Record<string, boolean> = {};
|
|
||||||
if (k === '*') {
|
|
||||||
if (!v) {
|
|
||||||
for (let exp of serverOptions.value.exporters) {
|
|
||||||
exclude[exp.key] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setBody({ ...body, exclude });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let exp of serverOptions.value.exporters) {
|
|
||||||
let val = body.exclude?.[exp.key];
|
|
||||||
if (k === exp.key) {
|
|
||||||
val = !v;
|
|
||||||
}
|
|
||||||
if (val) {
|
|
||||||
exclude[exp.key] = val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setBody({ ...body, exclude });
|
|
||||||
},
|
|
||||||
[body, setBody, serverOptions]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const subscription = getGrafanaLiveSrv()
|
|
||||||
.getStream<ExportStatusMessage>({
|
|
||||||
scope: LiveChannelScope.Grafana,
|
|
||||||
namespace: 'broadcast',
|
|
||||||
path: 'export',
|
|
||||||
})
|
|
||||||
.subscribe({
|
|
||||||
next: (evt) => {
|
|
||||||
if (isLiveChannelMessageEvent(evt)) {
|
|
||||||
setStatus(evt.message);
|
|
||||||
} else if (isLiveChannelStatusEvent(evt)) {
|
|
||||||
setStatus(evt.message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
subscription.unsubscribe();
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const renderView = () => {
|
|
||||||
const isEntityStoreEnabled = body?.format === StorageFormat.EntityStore && config.featureToggles.entityStore;
|
|
||||||
const shouldDisplayContent = isEntityStoreEnabled || body?.format === StorageFormat.Git;
|
|
||||||
|
|
||||||
const statusFragment = status && (
|
|
||||||
<div>
|
|
||||||
<h3>Status</h3>
|
|
||||||
<pre>{JSON.stringify(status, null, 2)}</pre>
|
|
||||||
{status.running && (
|
|
||||||
<div>
|
|
||||||
<Button variant="secondary" onClick={doStop}>
|
|
||||||
Stop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const formFragment = !Boolean(status?.running) && (
|
|
||||||
<div>
|
|
||||||
<Field label="Format">
|
|
||||||
<Select
|
|
||||||
options={formats}
|
|
||||||
width={40}
|
|
||||||
value={formats.find((v) => v.value === body?.format)}
|
|
||||||
onChange={(v) => setBody({ ...body!, format: v.value! })}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
{!isEntityStoreEnabled && body?.format !== StorageFormat.Git && (
|
|
||||||
<div>
|
|
||||||
<Alert title="Missing feature flag">Enable the `entityStore` feature flag</Alert>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{body?.format === StorageFormat.Git && (
|
|
||||||
<>
|
|
||||||
<Field label="Keep history">
|
|
||||||
<Switch value={body?.history} onChange={(v) => setBody({ ...body!, history: v.currentTarget.checked })} />
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Include">
|
|
||||||
<>
|
|
||||||
<InlineFieldRow>
|
|
||||||
<InlineField label="Toggle all" labelWidth={labelWith}>
|
|
||||||
<InlineSwitch
|
|
||||||
value={Object.keys(body?.exclude ?? {}).length === 0}
|
|
||||||
onChange={(v) => setInclude('*', v.currentTarget.checked)}
|
|
||||||
/>
|
|
||||||
</InlineField>
|
|
||||||
</InlineFieldRow>
|
|
||||||
{serverOptions.value && (
|
|
||||||
<div>
|
|
||||||
{serverOptions.value.exporters.map((ex) => (
|
|
||||||
<InlineFieldRow key={ex.key}>
|
|
||||||
<InlineField label={ex.name} labelWidth={labelWith} tooltip={ex.description}>
|
|
||||||
<InlineSwitch
|
|
||||||
value={body?.exclude?.[ex.key] !== true}
|
|
||||||
onChange={(v) => setInclude(ex.key, v.currentTarget.checked)}
|
|
||||||
/>
|
|
||||||
</InlineField>
|
|
||||||
</InlineFieldRow>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</Field>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{shouldDisplayContent && (
|
|
||||||
<>
|
|
||||||
<Field label="General folder" description="Set the folder name for items without a real folder">
|
|
||||||
<Input
|
|
||||||
width={40}
|
|
||||||
value={body?.generalFolderPath ?? ''}
|
|
||||||
onChange={(v) => setBody({ ...body!, generalFolderPath: v.currentTarget.value })}
|
|
||||||
placeholder="root folder path"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<HorizontalGroup>
|
|
||||||
<Button onClick={doStart} variant="primary">
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
<LinkButton href="admin/storage/" variant="secondary">
|
|
||||||
Cancel
|
|
||||||
</LinkButton>
|
|
||||||
</HorizontalGroup>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const requestDetailsFragment = (isEntityStoreEnabled || body?.format === StorageFormat.Git) && (
|
|
||||||
<Collapse label="Request details" isOpen={details} onToggle={setDetails} collapsible={true}>
|
|
||||||
<CodeEditor
|
|
||||||
height={275}
|
|
||||||
value={JSON.stringify(body, null, 2) ?? ''}
|
|
||||||
showLineNumbers={false}
|
|
||||||
readOnly={false}
|
|
||||||
language="json"
|
|
||||||
showMiniMap={false}
|
|
||||||
onBlur={(text: string) => {
|
|
||||||
setBody(JSON.parse(text)); // force JSON?
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Collapse>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{statusFragment}
|
|
||||||
{formFragment}
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
{requestDetailsFragment}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page navModel={navModel}>
|
|
||||||
<Page.Contents>{renderView()}</Page.Contents>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
@ -366,13 +366,6 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
() => import(/* webpackChunkName: "AdminEditOrgPage" */ 'app/features/admin/AdminEditOrgPage')
|
() => import(/* webpackChunkName: "AdminEditOrgPage" */ 'app/features/admin/AdminEditOrgPage')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/admin/storage/export',
|
|
||||||
roles: () => ['Admin'],
|
|
||||||
component: SafeDynamicImport(
|
|
||||||
() => import(/* webpackChunkName: "ExportPage" */ 'app/features/storage/ExportPage')
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/admin/storage/:path*',
|
path: '/admin/storage/:path*',
|
||||||
roles: () => ['Admin'],
|
roles: () => ['Admin'],
|
||||||
|
Reference in New Issue
Block a user