dashboard_version.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. package sqlstore
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "time"
  6. "github.com/go-xorm/xorm"
  7. "github.com/grafana/grafana/pkg/bus"
  8. "github.com/grafana/grafana/pkg/components/formatter"
  9. "github.com/grafana/grafana/pkg/components/simplejson"
  10. m "github.com/grafana/grafana/pkg/models"
  11. diff "github.com/yudai/gojsondiff"
  12. deltaFormatter "github.com/yudai/gojsondiff/formatter"
  13. )
  14. var (
  15. // ErrUnsupportedDiffType occurs when an invalid diff type is used.
  16. ErrUnsupportedDiffType = errors.New("sqlstore: unsupported diff type")
  17. // ErrNilDiff occurs when two compared interfaces are identical.
  18. ErrNilDiff = errors.New("sqlstore: diff is nil")
  19. )
  20. func init() {
  21. bus.AddHandler("sql", CompareDashboardVersionsCommand)
  22. bus.AddHandler("sql", GetDashboardVersion)
  23. bus.AddHandler("sql", GetDashboardVersions)
  24. bus.AddHandler("sql", RestoreDashboardVersion)
  25. }
  26. // CompareDashboardVersionsCommand computes the JSON diff of two versions,
  27. // assigning the delta of the diff to the `Delta` field.
  28. func CompareDashboardVersionsCommand(cmd *m.CompareDashboardVersionsCommand) error {
  29. original, err := getDashboardVersion(cmd.DashboardId, cmd.Original)
  30. if err != nil {
  31. return err
  32. }
  33. newDashboard, err := getDashboardVersion(cmd.DashboardId, cmd.New)
  34. if err != nil {
  35. return err
  36. }
  37. left, jsonDiff, err := getDiff(original, newDashboard)
  38. if err != nil {
  39. return err
  40. }
  41. switch cmd.DiffType {
  42. case m.DiffDelta:
  43. deltaOutput, err := deltaFormatter.NewDeltaFormatter().Format(jsonDiff)
  44. if err != nil {
  45. return err
  46. }
  47. cmd.Delta = []byte(deltaOutput)
  48. case m.DiffJSON:
  49. jsonOutput, err := formatter.NewJSONFormatter(left).Format(jsonDiff)
  50. if err != nil {
  51. return err
  52. }
  53. cmd.Delta = []byte(jsonOutput)
  54. case m.DiffBasic:
  55. basicOutput, err := formatter.NewBasicFormatter(left).Format(jsonDiff)
  56. if err != nil {
  57. return err
  58. }
  59. cmd.Delta = basicOutput
  60. default:
  61. return ErrUnsupportedDiffType
  62. }
  63. return nil
  64. }
  65. // GetDashboardVersion gets the dashboard version for the given dashboard ID
  66. // and version number.
  67. func GetDashboardVersion(query *m.GetDashboardVersionCommand) error {
  68. result, err := getDashboardVersion(query.DashboardId, query.Version)
  69. if err != nil {
  70. return err
  71. }
  72. query.Result = result
  73. return nil
  74. }
  75. // GetDashboardVersions gets all dashboard versions for the given dashboard ID.
  76. func GetDashboardVersions(query *m.GetDashboardVersionsCommand) error {
  77. order := ""
  78. // the query builder in xorm doesn't provide a way to set
  79. // a default order, so we perform this check
  80. if query.OrderBy != "" {
  81. order = " desc"
  82. }
  83. err := x.In("dashboard_id", query.DashboardId).
  84. OrderBy(query.OrderBy+order).
  85. Limit(query.Limit, query.Start).
  86. Find(&query.Result)
  87. if err != nil {
  88. return err
  89. }
  90. if len(query.Result) < 1 {
  91. return m.ErrNoVersionsForDashboardId
  92. }
  93. return nil
  94. }
  95. // RestoreDashboardVersion restores the dashboard data to the given version.
  96. func RestoreDashboardVersion(cmd *m.RestoreDashboardVersionCommand) error {
  97. return inTransaction(func(sess *xorm.Session) error {
  98. // check if dashboard version exists in dashboard_version table
  99. //
  100. // normally we could use the getDashboardVersion func here, but since
  101. // we're in a transaction, we need to run the queries using the
  102. // session instead of using the global `x`, so we copy those functions
  103. // here, replacing `x` with `sess`
  104. dashboardVersion := m.DashboardVersion{}
  105. has, err := sess.Where(
  106. "dashboard_id=? AND version=?",
  107. cmd.DashboardId,
  108. cmd.Version,
  109. ).Get(&dashboardVersion)
  110. if err != nil {
  111. return err
  112. }
  113. if !has {
  114. return m.ErrDashboardVersionNotFound
  115. }
  116. dashboardVersion.Data.Set("id", dashboardVersion.DashboardId)
  117. // get the dashboard version
  118. dashboard := m.Dashboard{Id: cmd.DashboardId}
  119. has, err = sess.Get(&dashboard)
  120. if err != nil {
  121. return err
  122. }
  123. if has == false {
  124. return m.ErrDashboardNotFound
  125. }
  126. version, err := getMaxVersion(sess, dashboard.Id)
  127. if err != nil {
  128. return err
  129. }
  130. // revert and save to a new dashboard version
  131. dashboard.Data = dashboardVersion.Data
  132. dashboard.Updated = time.Now()
  133. dashboard.UpdatedBy = cmd.UserId
  134. dashboard.Version = version
  135. dashboard.Data.Set("version", dashboard.Version)
  136. affectedRows, err := sess.Id(dashboard.Id).Update(dashboard)
  137. if err != nil {
  138. return err
  139. }
  140. if affectedRows == 0 {
  141. return m.ErrDashboardNotFound
  142. }
  143. // save that version a new version
  144. dashVersion := &m.DashboardVersion{
  145. DashboardId: dashboard.Id,
  146. ParentVersion: cmd.Version,
  147. RestoredFrom: cmd.Version,
  148. Version: dashboard.Version,
  149. Created: time.Now(),
  150. CreatedBy: dashboard.UpdatedBy,
  151. Message: "",
  152. Data: dashboard.Data,
  153. }
  154. affectedRows, err = sess.Insert(dashVersion)
  155. if err != nil {
  156. return err
  157. }
  158. if affectedRows == 0 {
  159. return m.ErrDashboardNotFound
  160. }
  161. cmd.Result = &dashboard
  162. return nil
  163. })
  164. }
  165. // getDashboardVersion is a helper function that gets the dashboard version for
  166. // the given dashboard ID and version ID.
  167. func getDashboardVersion(dashboardId int64, version int) (*m.DashboardVersion, error) {
  168. dashboardVersion := m.DashboardVersion{}
  169. has, err := x.Where("dashboard_id=? AND version=?", dashboardId, version).Get(&dashboardVersion)
  170. if err != nil {
  171. return nil, err
  172. }
  173. if !has {
  174. return nil, m.ErrDashboardVersionNotFound
  175. }
  176. dashboardVersion.Data.Set("id", dashboardVersion.DashboardId)
  177. return &dashboardVersion, nil
  178. }
  179. // getDashboard gets a dashboard by ID. Used for retrieving the dashboard
  180. // associated with dashboard versions.
  181. func getDashboard(dashboardId int64) (*m.Dashboard, error) {
  182. dashboard := m.Dashboard{Id: dashboardId}
  183. has, err := x.Get(&dashboard)
  184. if err != nil {
  185. return nil, err
  186. }
  187. if has == false {
  188. return nil, m.ErrDashboardNotFound
  189. }
  190. return &dashboard, nil
  191. }
  192. // getDiff computes the diff of two dashboard versions.
  193. func getDiff(originalDash, newDash *m.DashboardVersion) (interface{}, diff.Diff, error) {
  194. leftBytes, err := simplejson.NewFromAny(originalDash).Encode()
  195. if err != nil {
  196. return nil, nil, err
  197. }
  198. rightBytes, err := simplejson.NewFromAny(newDash).Encode()
  199. if err != nil {
  200. return nil, nil, err
  201. }
  202. jsonDiff, err := diff.New().Compare(leftBytes, rightBytes)
  203. if err != nil {
  204. return nil, nil, err
  205. }
  206. if !jsonDiff.Modified() {
  207. return nil, nil, ErrNilDiff
  208. }
  209. left := make(map[string]interface{})
  210. err = json.Unmarshal(leftBytes, &left)
  211. return left, jsonDiff, nil
  212. }
  213. type version struct {
  214. Max int
  215. }
  216. // getMaxVersion returns the highest version number in the `dashboard_version`
  217. // table.
  218. //
  219. // This is necessary because sqlite3 doesn't support autoincrement in the same
  220. // way that Postgres or MySQL do, so we use this to get around that. Since it's
  221. // impossible to delete a version in Grafana, this is believed to be a
  222. // safe-enough alternative.
  223. func getMaxVersion(sess *xorm.Session, dashboardId int64) (int, error) {
  224. v := version{}
  225. has, err := sess.Table("dashboard_version").
  226. Select("MAX(version) AS max").
  227. Where("dashboard_id = ?", dashboardId).
  228. Get(&v)
  229. if !has {
  230. return 0, m.ErrDashboardNotFound
  231. }
  232. if err != nil {
  233. return 0, err
  234. }
  235. v.Max++
  236. return v.Max, nil
  237. }