mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' of github.com:torkelo/grafana-pro
Conflicts: grafana
This commit is contained in:
commit
f133a9de79
2
grafana
2
grafana
@ -1 +1 @@
|
||||
Subproject commit 3944c37627326a14c8d39d8f2f25617681e3f074
|
||||
Subproject commit 9b2476451ef341285e1387c6eefe97c7995e300a
|
2
install_dependencies.sh
Normal file
2
install_dependencies.sh
Normal file
@ -0,0 +1,2 @@
|
||||
go get code.google.com/p/goprotobuf/{proto,protoc-gen-go}
|
||||
|
@ -51,8 +51,8 @@ func (self *HttpServer) ListenAndServe() {
|
||||
}
|
||||
|
||||
// register default route
|
||||
self.router.GET("/", self.authMiddleware(), self.index)
|
||||
self.router.GET("/dashboard/*_", self.authMiddleware(), self.index)
|
||||
self.router.GET("/", self.auth(), self.index)
|
||||
self.router.GET("/dashboard/*_", self.auth(), self.index)
|
||||
|
||||
self.router.Run(":" + self.port)
|
||||
}
|
||||
|
@ -1,22 +1,24 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
log "github.com/alecthomas/log4go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/torkelo/grafana-pro/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
addRoutes(func(self *HttpServer) {
|
||||
self.router.GET("/api/dashboards/:id", self.getDashboard)
|
||||
self.router.GET("/api/search/", self.search)
|
||||
self.router.POST("/api/dashboard", self.postDashboard)
|
||||
self.router.GET("/api/dashboards/:id", self.auth(), self.getDashboard)
|
||||
self.router.GET("/api/search/", self.auth(), self.search)
|
||||
self.router.POST("/api/dashboard", self.auth(), self.postDashboard)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *HttpServer) getDashboard(c *gin.Context) {
|
||||
id := c.Params.ByName("id")
|
||||
accountId, err := c.Get("accountId")
|
||||
|
||||
dash, err := self.store.GetById(id)
|
||||
dash, err := self.store.GetDashboard(id, accountId.(int))
|
||||
if err != nil {
|
||||
c.JSON(404, newErrorResponse("Dashboard not found"))
|
||||
return
|
||||
@ -30,6 +32,7 @@ func (self *HttpServer) search(c *gin.Context) {
|
||||
|
||||
results, err := self.store.Query(query)
|
||||
if err != nil {
|
||||
log.Error("Store query error: %v", err)
|
||||
c.JSON(500, newErrorResponse("Failed"))
|
||||
return
|
||||
}
|
||||
@ -41,9 +44,19 @@ func (self *HttpServer) postDashboard(c *gin.Context) {
|
||||
var command saveDashboardCommand
|
||||
|
||||
if c.EnsureBody(&command) {
|
||||
err := self.store.Save(&models.Dashboard{Data: command.Dashboard})
|
||||
dashboard := models.NewDashboard("test")
|
||||
dashboard.Data = command.Dashboard
|
||||
dashboard.Title = dashboard.Data["title"].(string)
|
||||
dashboard.AccountId = 1
|
||||
dashboard.UpdateSlug()
|
||||
|
||||
if dashboard.Data["id"] != nil {
|
||||
dashboard.Id = dashboard.Data["id"].(string)
|
||||
}
|
||||
|
||||
err := self.store.SaveDashboard(dashboard)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{"status": "saved"})
|
||||
c.JSON(200, gin.H{"status": "success", "slug": dashboard.Slug})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -19,18 +19,28 @@ type loginJsonModel struct {
|
||||
func (self *HttpServer) loginPost(c *gin.Context) {
|
||||
var loginModel loginJsonModel
|
||||
|
||||
if c.EnsureBody(&loginModel) {
|
||||
if loginModel.Email == "manu" && loginModel.Password == "123" {
|
||||
|
||||
session, _ := sessionStore.Get(c.Request, "grafana-session")
|
||||
session.Values["login"] = true
|
||||
session.Save(c.Request, c.Writer)
|
||||
|
||||
c.JSON(200, gin.H{"status": "you are logged in"})
|
||||
} else {
|
||||
c.JSON(401, gin.H{"status": "unauthorized"})
|
||||
}
|
||||
if !c.EnsureBody(&loginModel) {
|
||||
c.JSON(400, gin.H{"status": "bad request"})
|
||||
return
|
||||
}
|
||||
|
||||
account, err := self.store.GetUserAccountLogin(loginModel.Email)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"status": "some error"})
|
||||
}
|
||||
|
||||
if loginModel.Password != account.Password {
|
||||
c.JSON(401, gin.H{"status": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
session, _ := sessionStore.Get(c.Request, "grafana-session")
|
||||
session.Values["login"] = true
|
||||
session.Values["accountId"] = account.DatabaseId
|
||||
|
||||
session.Save(c.Request, c.Writer)
|
||||
|
||||
c.JSON(200, gin.H{"status": "you are logged in"})
|
||||
}
|
||||
|
||||
func (self *HttpServer) logoutPost(c *gin.Context) {
|
||||
@ -41,15 +51,18 @@ func (self *HttpServer) logoutPost(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "logged out"})
|
||||
}
|
||||
|
||||
func (self *HttpServer) authMiddleware() gin.HandlerFunc {
|
||||
func (self *HttpServer) auth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session, _ := sessionStore.Get(c.Request, "grafana-session")
|
||||
|
||||
if c.Request.URL.Path != "/login" && session.Values["login"] == nil {
|
||||
c.Writer.Header().Set("Location", "/login")
|
||||
c.Abort(302)
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("accountId", session.Values["accountId"])
|
||||
|
||||
session.Save(c.Request, c.Writer)
|
||||
}
|
||||
}
|
||||
|
45
pkg/api/api_register.go
Normal file
45
pkg/api/api_register.go
Normal file
@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
log "github.com/alecthomas/log4go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/torkelo/grafana-pro/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
addRoutes(func(self *HttpServer) {
|
||||
self.router.GET("/register/*_", self.index)
|
||||
self.router.POST("/api/register/user", self.registerUserPost)
|
||||
})
|
||||
}
|
||||
|
||||
type registerAccountJsonModel struct {
|
||||
Email string `json:"email" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Password2 bool `json:"remember2"`
|
||||
}
|
||||
|
||||
func (self *HttpServer) registerUserPost(c *gin.Context) {
|
||||
var registerModel registerAccountJsonModel
|
||||
|
||||
if !c.EnsureBody(®isterModel) {
|
||||
c.JSON(400, gin.H{"status": "bad request"})
|
||||
return
|
||||
}
|
||||
|
||||
account := models.UserAccount{
|
||||
UserName: registerModel.Email,
|
||||
Login: registerModel.Email,
|
||||
Email: registerModel.Email,
|
||||
Password: registerModel.Password,
|
||||
}
|
||||
|
||||
err := self.store.SaveUserAccount(&account)
|
||||
if err != nil {
|
||||
log.Error("Failed to create user account, email: %v, error: %v", registerModel.Email, err)
|
||||
c.JSON(500, gin.H{"status": "failed to create account"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
}
|
18
pkg/models/dashboard_test.go
Normal file
18
pkg/models/dashboard_test.go
Normal file
@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDashboardModel(t *testing.T) {
|
||||
|
||||
Convey("When generating slug", t, func() {
|
||||
dashboard := NewDashboard("Grafana Play Home")
|
||||
dashboard.UpdateSlug()
|
||||
|
||||
So(dashboard.Slug, ShouldEqual, "grafana-play-home")
|
||||
})
|
||||
|
||||
}
|
@ -3,22 +3,65 @@ package models
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Dashboard struct {
|
||||
Data map[string]interface{}
|
||||
Id string `gorethink:"id,omitempty"`
|
||||
Slug string
|
||||
AccountId int
|
||||
LastModifiedByUserId string
|
||||
LastModifiedByDate time.Time
|
||||
CreatedDate time.Time
|
||||
|
||||
Title string
|
||||
Tags []string
|
||||
Data map[string]interface{}
|
||||
}
|
||||
|
||||
type UserAccountLink struct {
|
||||
UserId int
|
||||
Role string
|
||||
ModifiedOn time.Time
|
||||
CreatedOn time.Time
|
||||
}
|
||||
|
||||
type UserAccount struct {
|
||||
DatabaseId int `gorethink:"id"`
|
||||
UserName string
|
||||
Login string
|
||||
Email string
|
||||
Password string
|
||||
NextDashboardId int
|
||||
UsingAccountId int
|
||||
GrantedAccess []UserAccountLink
|
||||
CreatedOn time.Time
|
||||
ModifiedOn time.Time
|
||||
}
|
||||
|
||||
type UserContext struct {
|
||||
UserId string
|
||||
AccountId string
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
Type string `json:"title"`
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
|
||||
func NewDashboard(title string) *Dashboard {
|
||||
dash := &Dashboard{}
|
||||
dash.Id = ""
|
||||
dash.LastModifiedByDate = time.Now()
|
||||
dash.CreatedDate = time.Now()
|
||||
dash.LastModifiedByUserId = "123"
|
||||
dash.Data = make(map[string]interface{})
|
||||
dash.Data["title"] = title
|
||||
dash.Title = title
|
||||
dash.UpdateSlug()
|
||||
|
||||
return dash
|
||||
}
|
||||
@ -34,20 +77,13 @@ func NewFromJson(reader io.Reader) (*Dashboard, error) {
|
||||
return dash, nil
|
||||
}
|
||||
|
||||
/*type DashboardServices struct {
|
||||
}
|
||||
|
||||
type DashboardServicesFilter struct {
|
||||
}
|
||||
|
||||
type DashboardServicesFilterTime struct {
|
||||
From string To string
|
||||
}*/
|
||||
|
||||
func (dash *Dashboard) GetString(prop string) string {
|
||||
return dash.Data[prop].(string)
|
||||
}
|
||||
|
||||
func (dash *Dashboard) Title() string {
|
||||
return dash.GetString("title")
|
||||
func (dash *Dashboard) UpdateSlug() {
|
||||
title := strings.ToLower(dash.Data["title"].(string))
|
||||
re := regexp.MustCompile("[^\\w ]+")
|
||||
re2 := regexp.MustCompile("\\s")
|
||||
dash.Slug = re2.ReplaceAllString(re.ReplaceAllString(title, ""), "-")
|
||||
}
|
||||
|
@ -1,155 +1,156 @@
|
||||
package stores
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
log "github.com/alecthomas/log4go"
|
||||
"github.com/torkelo/grafana-pro/pkg/models"
|
||||
)
|
||||
|
||||
type fileStore struct {
|
||||
dataDir string
|
||||
dashDir string
|
||||
cache map[string]*models.Dashboard
|
||||
}
|
||||
|
||||
func NewFileStore(dataDir string) *fileStore {
|
||||
|
||||
if dirDoesNotExist(dataDir) {
|
||||
log.Crashf("FileStore failed to initialize, dataDir does not exist %v", dataDir)
|
||||
}
|
||||
|
||||
dashDir := filepath.Join(dataDir, "dashboards")
|
||||
|
||||
if dirDoesNotExist(dashDir) {
|
||||
log.Debug("Did not find dashboard dir, creating...")
|
||||
err := os.Mkdir(dashDir, 0777)
|
||||
if err != nil {
|
||||
log.Crashf("FileStore failed to initialize, could not create directory %v, error: %v", dashDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
store := &fileStore{}
|
||||
store.dataDir = dataDir
|
||||
store.dashDir = dashDir
|
||||
store.cache = make(map[string]*models.Dashboard)
|
||||
go store.scanFiles()
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
func (store *fileStore) scanFiles() {
|
||||
visitor := func(path string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(f.Name(), ".json") {
|
||||
err = store.loadDashboardIntoCache(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err := filepath.Walk(store.dashDir, visitor)
|
||||
if err != nil {
|
||||
log.Error("FileStore::updateCache failed %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (store fileStore) loadDashboardIntoCache(filename string) error {
|
||||
log.Info("Loading dashboard file %v into cache", filename)
|
||||
dash, err := loadDashboardFromFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store.cache[dash.Title()] = dash
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *fileStore) Close() {
|
||||
|
||||
}
|
||||
|
||||
func (store *fileStore) GetById(id string) (*models.Dashboard, error) {
|
||||
log.Debug("FileStore::GetById id = %v", id)
|
||||
filename := store.getFilePathForDashboard(id)
|
||||
|
||||
return loadDashboardFromFile(filename)
|
||||
}
|
||||
|
||||
func (store *fileStore) Save(dash *models.Dashboard) error {
|
||||
filename := store.getFilePathForDashboard(dash.Title())
|
||||
|
||||
log.Debug("Saving dashboard %v to %v", dash.Title(), filename)
|
||||
|
||||
var err error
|
||||
var data []byte
|
||||
if data, err = json.Marshal(dash.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeFile(filename, data)
|
||||
}
|
||||
|
||||
func (store *fileStore) Query(query string) ([]*models.SearchResult, error) {
|
||||
results := make([]*models.SearchResult, 0, 50)
|
||||
|
||||
for _, dash := range store.cache {
|
||||
item := &models.SearchResult{
|
||||
Id: dash.Title(),
|
||||
Type: "dashboard",
|
||||
}
|
||||
results = append(results, item)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func loadDashboardFromFile(filename string) (*models.Dashboard, error) {
|
||||
log.Debug("FileStore::loading dashboard from file %v", filename)
|
||||
|
||||
configFile, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return models.NewFromJson(configFile)
|
||||
}
|
||||
|
||||
func (store *fileStore) getFilePathForDashboard(id string) string {
|
||||
id = strings.ToLower(id)
|
||||
id = strings.Replace(id, " ", "-", -1)
|
||||
return filepath.Join(store.dashDir, id) + ".json"
|
||||
}
|
||||
|
||||
func dirDoesNotExist(dir string) bool {
|
||||
_, err := os.Stat(dir)
|
||||
return os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func writeFile(filename string, data []byte) error {
|
||||
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := f.Write(data)
|
||||
if err == nil && n < len(data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
if err1 := f.Close(); err == nil {
|
||||
err = err1
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
//
|
||||
// import (
|
||||
// "encoding/json"
|
||||
// "io"
|
||||
// "os"
|
||||
// "path/filepath"
|
||||
// "strings"
|
||||
//
|
||||
// log "github.com/alecthomas/log4go"
|
||||
// "github.com/torkelo/grafana-pro/pkg/models"
|
||||
// )
|
||||
//
|
||||
// type fileStore struct {
|
||||
// dataDir string
|
||||
// dashDir string
|
||||
// cache map[string]*models.Dashboard
|
||||
// }
|
||||
//
|
||||
// func NewFileStore(dataDir string) *fileStore {
|
||||
//
|
||||
// if dirDoesNotExist(dataDir) {
|
||||
// log.Crashf("FileStore failed to initialize, dataDir does not exist %v", dataDir)
|
||||
// }
|
||||
//
|
||||
// dashDir := filepath.Join(dataDir, "dashboards")
|
||||
//
|
||||
// if dirDoesNotExist(dashDir) {
|
||||
// log.Debug("Did not find dashboard dir, creating...")
|
||||
// err := os.Mkdir(dashDir, 0777)
|
||||
// if err != nil {
|
||||
// log.Crashf("FileStore failed to initialize, could not create directory %v, error: %v", dashDir, err)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// store := &fileStore{}
|
||||
// store.dataDir = dataDir
|
||||
// store.dashDir = dashDir
|
||||
// store.cache = make(map[string]*models.Dashboard)
|
||||
// store.scanFiles()
|
||||
//
|
||||
// return store
|
||||
// }
|
||||
//
|
||||
// func (store *fileStore) scanFiles() {
|
||||
// visitor := func(path string, f os.FileInfo, err error) error {
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if f.IsDir() {
|
||||
// return nil
|
||||
// }
|
||||
// if strings.HasSuffix(f.Name(), ".json") {
|
||||
// err = store.loadDashboardIntoCache(path)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// err := filepath.Walk(store.dashDir, visitor)
|
||||
// if err != nil {
|
||||
// log.Error("FileStore::updateCache failed %v", err)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func (store fileStore) loadDashboardIntoCache(filename string) error {
|
||||
// log.Info("Loading dashboard file %v into cache", filename)
|
||||
// dash, err := loadDashboardFromFile(filename)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// store.cache[dash.Title] = dash
|
||||
//
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// func (store *fileStore) Close() {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// func (store *fileStore) GetById(id string) (*models.Dashboard, error) {
|
||||
// log.Debug("FileStore::GetById id = %v", id)
|
||||
// filename := store.getFilePathForDashboard(id)
|
||||
//
|
||||
// return loadDashboardFromFile(filename)
|
||||
// }
|
||||
//
|
||||
// func (store *fileStore) Save(dash *models.Dashboard) error {
|
||||
// filename := store.getFilePathForDashboard(dash.Title)
|
||||
//
|
||||
// log.Debug("Saving dashboard %v to %v", dash.Title, filename)
|
||||
//
|
||||
// var err error
|
||||
// var data []byte
|
||||
// if data, err = json.Marshal(dash.Data); err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// return writeFile(filename, data)
|
||||
// }
|
||||
//
|
||||
// func (store *fileStore) Query(query string) ([]*models.SearchResult, error) {
|
||||
// results := make([]*models.SearchResult, 0, 50)
|
||||
//
|
||||
// for _, dash := range store.cache {
|
||||
// item := &models.SearchResult{
|
||||
// Id: dash.Title,
|
||||
// Type: "dashboard",
|
||||
// }
|
||||
// results = append(results, item)
|
||||
// }
|
||||
//
|
||||
// return results, nil
|
||||
// }
|
||||
//
|
||||
// func loadDashboardFromFile(filename string) (*models.Dashboard, error) {
|
||||
// log.Debug("FileStore::loading dashboard from file %v", filename)
|
||||
//
|
||||
// configFile, err := os.Open(filename)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// return models.NewFromJson(configFile)
|
||||
// }
|
||||
//
|
||||
// func (store *fileStore) getFilePathForDashboard(id string) string {
|
||||
// id = strings.ToLower(id)
|
||||
// id = strings.Replace(id, " ", "-", -1)
|
||||
// return filepath.Join(store.dashDir, id) + ".json"
|
||||
// }
|
||||
//
|
||||
// func dirDoesNotExist(dir string) bool {
|
||||
// _, err := os.Stat(dir)
|
||||
// return os.IsNotExist(err)
|
||||
// }
|
||||
//
|
||||
// func writeFile(filename string, data []byte) error {
|
||||
// f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// n, err := f.Write(data)
|
||||
// if err == nil && n < len(data) {
|
||||
// err = io.ErrShortWrite
|
||||
// }
|
||||
// if err1 := f.Close(); err == nil {
|
||||
// err = err1
|
||||
// }
|
||||
//
|
||||
// return err
|
||||
// }
|
||||
|
@ -1,112 +1,113 @@
|
||||
package stores
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/torkelo/grafana-pro/pkg/models"
|
||||
)
|
||||
|
||||
func TestFileStore(t *testing.T) {
|
||||
|
||||
GivenFileStore("When saving a dashboard", t, func(store *fileStore) {
|
||||
dashboard := models.NewDashboard("hello")
|
||||
|
||||
err := store.Save(dashboard)
|
||||
|
||||
Convey("should be saved to disk", func() {
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = os.Stat(store.getFilePathForDashboard("hello"))
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
GivenFileStore("When getting a saved dashboard", t, func(store *fileStore) {
|
||||
copyDashboardToTempData("default.json", "", store.dashDir)
|
||||
dash, err := store.GetById("default")
|
||||
|
||||
Convey("should be read from disk", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(dash, ShouldNotBeNil)
|
||||
|
||||
So(dash.Title(), ShouldEqual, "Grafana Play Home")
|
||||
})
|
||||
})
|
||||
|
||||
GivenFileStore("when getting dashboard with capital letters", t, func(store *fileStore) {
|
||||
copyDashboardToTempData("annotations.json", "", store.dashDir)
|
||||
dash, err := store.GetById("AnnoTations")
|
||||
|
||||
Convey("should be read from disk", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(dash, ShouldNotBeNil)
|
||||
|
||||
So(dash.Title(), ShouldEqual, "Annotations")
|
||||
})
|
||||
})
|
||||
|
||||
GivenFileStore("When copying dashboards into data dir", t, func(store *fileStore) {
|
||||
copyDashboardToTempData("annotations.json", "", store.dashDir)
|
||||
copyDashboardToTempData("default.json", "", store.dashDir)
|
||||
copyDashboardToTempData("graph-styles.json", "", store.dashDir)
|
||||
store.scanFiles()
|
||||
|
||||
Convey("scan should generate index of all dashboards", func() {
|
||||
|
||||
result, err := store.Query("*")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(result), ShouldEqual, 3)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func copyDashboardToTempData(name string, destName string, dir string) {
|
||||
if destName == "" {
|
||||
destName = name
|
||||
}
|
||||
source, _ := filepath.Abs("../../data/dashboards/" + name)
|
||||
dest := filepath.Join(dir, destName)
|
||||
err := copyFile(dest, source)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to copy file %v", name))
|
||||
}
|
||||
}
|
||||
|
||||
func GivenFileStore(desc string, t *testing.T, f func(store *fileStore)) {
|
||||
Convey(desc, t, func() {
|
||||
tempDir, _ := ioutil.TempDir("", "store")
|
||||
|
||||
store := NewFileStore(tempDir)
|
||||
|
||||
f(store)
|
||||
|
||||
Reset(func() {
|
||||
os.RemoveAll(tempDir)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func copyFile(dst, src string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, in)
|
||||
cerr := out.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cerr
|
||||
}
|
||||
//
|
||||
// import (
|
||||
// "fmt"
|
||||
// "io"
|
||||
// "io/ioutil"
|
||||
// "os"
|
||||
// "path/filepath"
|
||||
// "testing"
|
||||
//
|
||||
// . "github.com/smartystreets/goconvey/convey"
|
||||
// "github.com/torkelo/grafana-pro/pkg/models"
|
||||
// )
|
||||
//
|
||||
// func TestFileStore(t *testing.T) {
|
||||
//
|
||||
// GivenFileStore("When saving a dashboard", t, func(store *fileStore) {
|
||||
// dashboard := models.NewDashboard("hello")
|
||||
//
|
||||
// err := store.Save(dashboard)
|
||||
//
|
||||
// Convey("should be saved to disk", func() {
|
||||
// So(err, ShouldBeNil)
|
||||
//
|
||||
// _, err = os.Stat(store.getFilePathForDashboard("hello"))
|
||||
// So(err, ShouldBeNil)
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// GivenFileStore("When getting a saved dashboard", t, func(store *fileStore) {
|
||||
// copyDashboardToTempData("default.json", "", store.dashDir)
|
||||
// dash, err := store.GetById("default")
|
||||
//
|
||||
// Convey("should be read from disk", func() {
|
||||
// So(err, ShouldBeNil)
|
||||
// So(dash, ShouldNotBeNil)
|
||||
//
|
||||
// So(dash.Title, ShouldEqual, "Grafana Play Home")
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// GivenFileStore("when getting dashboard with capital letters", t, func(store *fileStore) {
|
||||
// copyDashboardToTempData("annotations.json", "", store.dashDir)
|
||||
// dash, err := store.GetById("AnnoTations")
|
||||
//
|
||||
// Convey("should be read from disk", func() {
|
||||
// So(err, ShouldBeNil)
|
||||
// So(dash, ShouldNotBeNil)
|
||||
//
|
||||
// So(dash.Title, ShouldEqual, "Annotations")
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// GivenFileStore("When copying dashboards into data dir", t, func(store *fileStore) {
|
||||
// copyDashboardToTempData("annotations.json", "", store.dashDir)
|
||||
// copyDashboardToTempData("default.json", "", store.dashDir)
|
||||
// copyDashboardToTempData("graph-styles.json", "", store.dashDir)
|
||||
// store.scanFiles()
|
||||
//
|
||||
// Convey("scan should generate index of all dashboards", func() {
|
||||
//
|
||||
// result, err := store.Query("*")
|
||||
// So(err, ShouldBeNil)
|
||||
// So(len(result), ShouldEqual, 3)
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// func copyDashboardToTempData(name string, destName string, dir string) {
|
||||
// if destName == "" {
|
||||
// destName = name
|
||||
// }
|
||||
// source, _ := filepath.Abs("../../data/dashboards/" + name)
|
||||
// dest := filepath.Join(dir, destName)
|
||||
// err := copyFile(dest, source)
|
||||
// if err != nil {
|
||||
// panic(fmt.Sprintf("failed to copy file %v", name))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func GivenFileStore(desc string, t *testing.T, f func(store *fileStore)) {
|
||||
// Convey(desc, t, func() {
|
||||
// tempDir, _ := ioutil.TempDir("", "store")
|
||||
//
|
||||
// store := NewFileStore(tempDir)
|
||||
//
|
||||
// f(store)
|
||||
//
|
||||
// Reset(func() {
|
||||
// os.RemoveAll(tempDir)
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// func copyFile(dst, src string) error {
|
||||
// in, err := os.Open(src)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer in.Close()
|
||||
// out, err := os.Create(dst)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer out.Close()
|
||||
// _, err = io.Copy(out, in)
|
||||
// cerr := out.Close()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return cerr
|
||||
// }
|
||||
|
110
pkg/stores/rethinkdb.go
Normal file
110
pkg/stores/rethinkdb.go
Normal file
@ -0,0 +1,110 @@
|
||||
package stores
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
log "github.com/alecthomas/log4go"
|
||||
r "github.com/dancannon/gorethink"
|
||||
"github.com/torkelo/grafana-pro/pkg/models"
|
||||
)
|
||||
|
||||
type rethinkStore struct {
|
||||
session *r.Session
|
||||
}
|
||||
|
||||
type RethinkCfg struct {
|
||||
DatabaseName string
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
Id int `gorethink:"id"`
|
||||
NextDashboardId int
|
||||
}
|
||||
|
||||
func NewRethinkStore(config *RethinkCfg) *rethinkStore {
|
||||
log.Info("Initializing rethink storage")
|
||||
|
||||
session, err := r.Connect(r.ConnectOpts{
|
||||
Address: "localhost:28015",
|
||||
Database: config.DatabaseName,
|
||||
MaxIdle: 10,
|
||||
IdleTimeout: time.Second * 10,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Crash("Failed to connect to rethink database %v", err)
|
||||
}
|
||||
|
||||
r.DbCreate(config.DatabaseName).Exec(session)
|
||||
r.Db(config.DatabaseName).TableCreate("dashboards").Exec(session)
|
||||
r.Db(config.DatabaseName).TableCreate("accounts").Exec(session)
|
||||
r.Db(config.DatabaseName).TableCreate("master").Exec(session)
|
||||
|
||||
r.Db(config.DatabaseName).Table("dashboards").IndexCreateFunc("AccountIdSlug", func(row r.Term) interface{} {
|
||||
return []interface{}{row.Field("AccountId"), row.Field("Slug")}
|
||||
}).Exec(session)
|
||||
|
||||
r.Db(config.DatabaseName).Table("accounts").IndexCreateFunc("AccountLogin", func(row r.Term) interface{} {
|
||||
return []interface{}{row.Field("Login")}
|
||||
}).Exec(session)
|
||||
|
||||
_, err = r.Table("master").Insert(map[string]interface{}{"id": "ids", "NextAccountId": 0}).RunWrite(session)
|
||||
if err != nil {
|
||||
log.Error("Failed to insert master ids row", err)
|
||||
}
|
||||
|
||||
return &rethinkStore{
|
||||
session: session,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error {
|
||||
resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Upsert: true}).RunWrite(self.session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("Inserted: %v, Errors: %v, Updated: %v", resp.Inserted, resp.Errors, resp.Updated)
|
||||
log.Info("First error:", resp.FirstError)
|
||||
if len(resp.GeneratedKeys) > 0 {
|
||||
dash.Id = resp.GeneratedKeys[0]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dashboard, error) {
|
||||
resp, err := r.Table("dashboards").GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).Run(self.session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dashboard models.Dashboard
|
||||
err = resp.One(&dashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dashboard, nil
|
||||
}
|
||||
|
||||
func (self *rethinkStore) Query(query string) ([]*models.SearchResult, error) {
|
||||
|
||||
docs, err := r.Table("dashboards").Filter(r.Row.Field("Title").Match(".*")).Run(self.session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make([]*models.SearchResult, 0, 50)
|
||||
var dashboard models.Dashboard
|
||||
for docs.Next(&dashboard) {
|
||||
results = append(results, &models.SearchResult{
|
||||
Title: dashboard.Title,
|
||||
Id: dashboard.Slug,
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (self *rethinkStore) Close() {}
|
76
pkg/stores/rethinkdb_accounts.go
Normal file
76
pkg/stores/rethinkdb_accounts.go
Normal file
@ -0,0 +1,76 @@
|
||||
package stores
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
r "github.com/dancannon/gorethink"
|
||||
"github.com/torkelo/grafana-pro/pkg/models"
|
||||
)
|
||||
|
||||
func (self *rethinkStore) getNextAccountId() (int, error) {
|
||||
resp, err := r.Table("master").Get("ids").Update(map[string]interface{}{
|
||||
"NextAccountId": r.Row.Field("NextAccountId").Add(1),
|
||||
}, r.UpdateOpts{ReturnVals: true}).RunWrite(self.session)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if resp.NewValue == nil {
|
||||
return 0, errors.New("Failed to get new value after incrementing account id")
|
||||
}
|
||||
|
||||
return int(resp.NewValue.(map[string]interface{})["NextAccountId"].(float64)), nil
|
||||
}
|
||||
|
||||
func (self *rethinkStore) SaveUserAccount(account *models.UserAccount) error {
|
||||
accountId, err := self.getNextAccountId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account.DatabaseId = accountId
|
||||
|
||||
resp, err := r.Table("accounts").Insert(account).RunWrite(self.session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Inserted == 0 {
|
||||
return errors.New("Failed to insert acccount")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *rethinkStore) GetUserAccountLogin(emailOrName string) (*models.UserAccount, error) {
|
||||
resp, err := r.Table("accounts").GetAllByIndex("AccountLogin", []interface{}{emailOrName}).Run(self.session)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var account models.UserAccount
|
||||
err = resp.One(&account)
|
||||
if err != nil {
|
||||
return nil, errors.New("Not found")
|
||||
}
|
||||
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (self *rethinkStore) getNextDashboardNumber(accountId int) (int, error) {
|
||||
resp, err := r.Table("accounts").Get(accountId).Update(map[string]interface{}{
|
||||
"NextDashboardId": r.Row.Field("NextDashboardId").Add(1),
|
||||
}, r.UpdateOpts{ReturnVals: true}).RunWrite(self.session)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if resp.NewValue == nil {
|
||||
return 0, errors.New("Failed to get next dashboard id, no new value after update")
|
||||
}
|
||||
|
||||
return int(resp.NewValue.(map[string]interface{})["NextDashboardId"].(float64)), nil
|
||||
}
|
56
pkg/stores/rethinkdb_test.go
Normal file
56
pkg/stores/rethinkdb_test.go
Normal file
@ -0,0 +1,56 @@
|
||||
package stores
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/dancannon/gorethink"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/torkelo/grafana-pro/pkg/models"
|
||||
)
|
||||
|
||||
func TestRethinkStore(t *testing.T) {
|
||||
store := NewRethinkStore(&RethinkCfg{DatabaseName: "tests"})
|
||||
defer gorethink.DbDrop("tests").Exec(store.session)
|
||||
|
||||
Convey("Insert dashboard", t, func() {
|
||||
dashboard := models.NewDashboard("test")
|
||||
dashboard.AccountId = 1
|
||||
|
||||
err := store.SaveDashboard(dashboard)
|
||||
So(err, ShouldBeNil)
|
||||
So(dashboard.Id, ShouldNotBeEmpty)
|
||||
|
||||
read, err := store.GetDashboard("test", 1)
|
||||
So(err, ShouldBeNil)
|
||||
So(read, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("can get next account id", t, func() {
|
||||
id, err := store.getNextAccountId()
|
||||
So(err, ShouldBeNil)
|
||||
So(id, ShouldNotEqual, 0)
|
||||
|
||||
id2, err := store.getNextAccountId()
|
||||
So(id2, ShouldEqual, id+1)
|
||||
})
|
||||
|
||||
Convey("can create account", t, func() {
|
||||
account := &models.UserAccount{UserName: "torkelo", Email: "mupp", Login: "test@test.com"}
|
||||
err := store.SaveUserAccount(account)
|
||||
So(err, ShouldBeNil)
|
||||
So(account.DatabaseId, ShouldNotEqual, 0)
|
||||
|
||||
read, err := store.GetUserAccountLogin("test@test.com")
|
||||
So(err, ShouldBeNil)
|
||||
So(read.DatabaseId, ShouldEqual, account.DatabaseId)
|
||||
})
|
||||
|
||||
Convey("can get next dashboard id", t, func() {
|
||||
account := &models.UserAccount{UserName: "torkelo", Email: "mupp"}
|
||||
err := store.SaveUserAccount(account)
|
||||
dashId, err := store.getNextDashboardNumber(account.DatabaseId)
|
||||
So(err, ShouldBeNil)
|
||||
So(dashId, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
}
|
@ -5,12 +5,14 @@ import (
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
GetById(id string) (*models.Dashboard, error)
|
||||
Save(dash *models.Dashboard) error
|
||||
GetDashboard(title string, accountId int) (*models.Dashboard, error)
|
||||
SaveDashboard(dash *models.Dashboard) error
|
||||
Query(query string) ([]*models.SearchResult, error)
|
||||
SaveUserAccount(acccount *models.UserAccount) error
|
||||
GetUserAccountLogin(emailOrName string) (*models.UserAccount, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
func New() Store {
|
||||
return NewFileStore("data")
|
||||
return NewRethinkStore(&RethinkCfg{DatabaseName: "grafana"})
|
||||
}
|
||||
|
7
start_dependencies.sh
Executable file
7
start_dependencies.sh
Executable file
@ -0,0 +1,7 @@
|
||||
docker kill gfdev
|
||||
docker rm gfdev
|
||||
|
||||
docker run -d -p 8180:8080 -p 28015:28015 -p 29015:29015 \
|
||||
--name rethinkdb \
|
||||
-v /var/docker/grafana-pro-rethinkdb:/data \
|
||||
dockerfile/rethinkdb
|
Loading…
Reference in New Issue
Block a user