The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/sqlstore/dashboard_version.go

274 lines
7.0 KiB

package sqlstore
import (
"encoding/json"
"errors"
"time"
"github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/formatter"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
diff "github.com/yudai/gojsondiff"
deltaFormatter "github.com/yudai/gojsondiff/formatter"
)
var (
// ErrUnsupportedDiffType occurs when an invalid diff type is used.
ErrUnsupportedDiffType = errors.New("sqlstore: unsupported diff type")
// ErrNilDiff occurs when two compared interfaces are identical.
ErrNilDiff = errors.New("sqlstore: diff is nil")
)
func init() {
bus.AddHandler("sql", CompareDashboardVersionsCommand)
bus.AddHandler("sql", GetDashboardVersion)
bus.AddHandler("sql", GetDashboardVersions)
bus.AddHandler("sql", RestoreDashboardVersion)
}
// CompareDashboardVersionsCommand computes the JSON diff of two versions,
// assigning the delta of the diff to the `Delta` field.
func CompareDashboardVersionsCommand(cmd *m.CompareDashboardVersionsCommand) error {
original, err := getDashboardVersion(cmd.DashboardId, cmd.Original)
if err != nil {
return err
}
newDashboard, err := getDashboardVersion(cmd.DashboardId, cmd.New)
if err != nil {
return err
}
left, jsonDiff, err := getDiff(original, newDashboard)
if err != nil {
return err
}
switch cmd.DiffType {
case m.DiffDelta:
deltaOutput, err := deltaFormatter.NewDeltaFormatter().Format(jsonDiff)
if err != nil {
return err
}
cmd.Delta = []byte(deltaOutput)
case m.DiffJSON:
jsonOutput, err := formatter.NewJSONFormatter(left).Format(jsonDiff)
if err != nil {
return err
}
cmd.Delta = []byte(jsonOutput)
case m.DiffBasic:
basicOutput, err := formatter.NewBasicFormatter(left).Format(jsonDiff)
if err != nil {
return err
}
cmd.Delta = basicOutput
default:
return ErrUnsupportedDiffType
}
return nil
}
// GetDashboardVersion gets the dashboard version for the given dashboard ID
// and version number.
func GetDashboardVersion(query *m.GetDashboardVersionCommand) error {
result, err := getDashboardVersion(query.DashboardId, query.Version)
if err != nil {
return err
}
query.Result = result
return nil
}
// GetDashboardVersions gets all dashboard versions for the given dashboard ID.
func GetDashboardVersions(query *m.GetDashboardVersionsCommand) error {
order := ""
// the query builder in xorm doesn't provide a way to set
// a default order, so we perform this check
if query.OrderBy != "" {
order = " desc"
}
err := x.In("dashboard_id", query.DashboardId).
OrderBy(query.OrderBy+order).
Limit(query.Limit, query.Start).
Find(&query.Result)
if err != nil {
return err
}
if len(query.Result) < 1 {
return m.ErrNoVersionsForDashboardId
}
return nil
}
// RestoreDashboardVersion restores the dashboard data to the given version.
func RestoreDashboardVersion(cmd *m.RestoreDashboardVersionCommand) error {
return inTransaction(func(sess *xorm.Session) error {
// check if dashboard version exists in dashboard_version table
//
// normally we could use the getDashboardVersion func here, but since
// we're in a transaction, we need to run the queries using the
// session instead of using the global `x`, so we copy those functions
// here, replacing `x` with `sess`
dashboardVersion := m.DashboardVersion{}
has, err := sess.Where(
"dashboard_id=? AND version=?",
cmd.DashboardId,
cmd.Version,
).Get(&dashboardVersion)
if err != nil {
return err
}
if !has {
return m.ErrDashboardVersionNotFound
}
dashboardVersion.Data.Set("id", dashboardVersion.DashboardId)
// get the dashboard version
dashboard := m.Dashboard{Id: cmd.DashboardId}
has, err = sess.Get(&dashboard)
if err != nil {
return err
}
if has == false {
return m.ErrDashboardNotFound
}
version, err := getMaxVersion(sess, dashboard.Id)
if err != nil {
return err
}
// revert and save to a new dashboard version
dashboard.Data = dashboardVersion.Data
dashboard.Updated = time.Now()
dashboard.UpdatedBy = cmd.UserId
dashboard.Version = version
dashboard.Data.Set("version", dashboard.Version)
affectedRows, err := sess.Id(dashboard.Id).Update(dashboard)
if err != nil {
return err
}
if affectedRows == 0 {
return m.ErrDashboardNotFound
}
// save that version a new version
dashVersion := &m.DashboardVersion{
DashboardId: dashboard.Id,
ParentVersion: cmd.Version,
RestoredFrom: cmd.Version,
Version: dashboard.Version,
Created: time.Now(),
CreatedBy: dashboard.UpdatedBy,
Message: "",
Data: dashboard.Data,
}
affectedRows, err = sess.Insert(dashVersion)
if err != nil {
return err
}
if affectedRows == 0 {
return m.ErrDashboardNotFound
}
cmd.Result = &dashboard
return nil
})
}
// getDashboardVersion is a helper function that gets the dashboard version for
// the given dashboard ID and version ID.
func getDashboardVersion(dashboardId int64, version int) (*m.DashboardVersion, error) {
dashboardVersion := m.DashboardVersion{}
has, err := x.Where("dashboard_id=? AND version=?", dashboardId, version).Get(&dashboardVersion)
if err != nil {
return nil, err
}
if !has {
return nil, m.ErrDashboardVersionNotFound
}
dashboardVersion.Data.Set("id", dashboardVersion.DashboardId)
return &dashboardVersion, nil
}
// getDashboard gets a dashboard by ID. Used for retrieving the dashboard
// associated with dashboard versions.
func getDashboard(dashboardId int64) (*m.Dashboard, error) {
dashboard := m.Dashboard{Id: dashboardId}
has, err := x.Get(&dashboard)
if err != nil {
return nil, err
}
if has == false {
return nil, m.ErrDashboardNotFound
}
return &dashboard, nil
}
// getDiff computes the diff of two dashboard versions.
func getDiff(originalDash, newDash *m.DashboardVersion) (interface{}, diff.Diff, error) {
leftBytes, err := simplejson.NewFromAny(originalDash).Encode()
if err != nil {
return nil, nil, err
}
rightBytes, err := simplejson.NewFromAny(newDash).Encode()
if err != nil {
return nil, nil, err
}
jsonDiff, err := diff.New().Compare(leftBytes, rightBytes)
if err != nil {
return nil, nil, err
}
if !jsonDiff.Modified() {
return nil, nil, ErrNilDiff
}
left := make(map[string]interface{})
err = json.Unmarshal(leftBytes, &left)
return left, jsonDiff, nil
}
type version struct {
Max int
}
// getMaxVersion returns the highest version number in the `dashboard_version`
// table.
//
// This is necessary because sqlite3 doesn't support autoincrement in the same
// way that Postgres or MySQL do, so we use this to get around that. Since it's
// impossible to delete a version in Grafana, this is believed to be a
// safe-enough alternative.
func getMaxVersion(sess *xorm.Session, dashboardId int64) (int, error) {
v := version{}
has, err := sess.Table("dashboard_version").
Select("MAX(version) AS max").
Where("dashboard_id = ?", dashboardId).
Get(&v)
if !has {
return 0, m.ErrDashboardNotFound
}
if err != nil {
return 0, err
}
v.Max++
return v.Max, nil
}