diff --git a/grafana b/grafana index 3944c376273..9b2476451ef 160000 --- a/grafana +++ b/grafana @@ -1 +1 @@ -Subproject commit 3944c37627326a14c8d39d8f2f25617681e3f074 +Subproject commit 9b2476451ef341285e1387c6eefe97c7995e300a diff --git a/install_dependencies.sh b/install_dependencies.sh new file mode 100644 index 00000000000..eafcee62380 --- /dev/null +++ b/install_dependencies.sh @@ -0,0 +1,2 @@ +go get code.google.com/p/goprotobuf/{proto,protoc-gen-go} + diff --git a/pkg/api/api.go b/pkg/api/api.go index 0ff584805ee..59c5079dde1 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.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) } diff --git a/pkg/api/api_dashboard.go b/pkg/api/api_dashboard.go index 60ecdc8189e..208b25c7b1f 100644 --- a/pkg/api/api_dashboard.go +++ b/pkg/api/api_dashboard.go @@ -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 } } diff --git a/pkg/api/api_login.go b/pkg/api/api_login.go index 284af4ff1d5..fa493ba65b9 100644 --- a/pkg/api/api_login.go +++ b/pkg/api/api_login.go @@ -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) } } diff --git a/pkg/api/api_register.go b/pkg/api/api_register.go new file mode 100644 index 00000000000..fdc0875df95 --- /dev/null +++ b/pkg/api/api_register.go @@ -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"}) +} diff --git a/pkg/models/dashboard_test.go b/pkg/models/dashboard_test.go new file mode 100644 index 00000000000..0828e51480f --- /dev/null +++ b/pkg/models/dashboard_test.go @@ -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") + }) + +} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 9f333aff8dc..53bd41d6bf8 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -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, ""), "-") } diff --git a/pkg/stores/file_store.go b/pkg/stores/file_store.go index 6b9765e6473..bf1c3f4f77e 100644 --- a/pkg/stores/file_store.go +++ b/pkg/stores/file_store.go @@ -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 +// } diff --git a/pkg/stores/file_store_test.go b/pkg/stores/file_store_test.go index 77c0224c53e..aff4673c7f5 100644 --- a/pkg/stores/file_store_test.go +++ b/pkg/stores/file_store_test.go @@ -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 +// } diff --git a/pkg/stores/rethinkdb.go b/pkg/stores/rethinkdb.go new file mode 100644 index 00000000000..abb974fdd29 --- /dev/null +++ b/pkg/stores/rethinkdb.go @@ -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() {} diff --git a/pkg/stores/rethinkdb_accounts.go b/pkg/stores/rethinkdb_accounts.go new file mode 100644 index 00000000000..e2a337146ae --- /dev/null +++ b/pkg/stores/rethinkdb_accounts.go @@ -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 +} diff --git a/pkg/stores/rethinkdb_test.go b/pkg/stores/rethinkdb_test.go new file mode 100644 index 00000000000..e90abb785e8 --- /dev/null +++ b/pkg/stores/rethinkdb_test.go @@ -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) + }) + +} diff --git a/pkg/stores/store.go b/pkg/stores/store.go index 8ce505c420e..8319e7ca86f 100644 --- a/pkg/stores/store.go +++ b/pkg/stores/store.go @@ -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"}) } diff --git a/start_dependencies.sh b/start_dependencies.sh new file mode 100755 index 00000000000..a8ecfd53c4c --- /dev/null +++ b/start_dependencies.sh @@ -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