From 07d8b542bf54028fbb3a05dd8bae55c76e2582bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 21 Aug 2014 22:09:48 +0200 Subject: [PATCH 1/3] working on rethinkdb stuff --- grafana | 2 +- install_dependencies.sh | 2 + pkg/api/api_dashboard.go | 16 +- pkg/models/dashboards.go | 39 +++-- pkg/stores/file_store.go | 307 +++++++++++++++++----------------- pkg/stores/file_store_test.go | 221 ++++++++++++------------ pkg/stores/rethinkdb.go | 94 +++++++++++ pkg/stores/rethinkdb_test.go | 29 ++++ pkg/stores/store.go | 6 +- start_dependencies.sh | 7 + 10 files changed, 437 insertions(+), 286 deletions(-) create mode 100644 install_dependencies.sh create mode 100644 pkg/stores/rethinkdb.go create mode 100644 pkg/stores/rethinkdb_test.go create mode 100755 start_dependencies.sh diff --git a/grafana b/grafana index 34ab1e529b4..06de80f2a76 160000 --- a/grafana +++ b/grafana @@ -1 +1 @@ -Subproject commit 34ab1e529b499af836631f8076c2c4df02be5860 +Subproject commit 06de80f2a764b76df6047fc4700c74aaf5734521 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_dashboard.go b/pkg/api/api_dashboard.go index 60ecdc8189e..5e2ca7c96d1 100644 --- a/pkg/api/api_dashboard.go +++ b/pkg/api/api_dashboard.go @@ -1,6 +1,7 @@ package api import ( + log "github.com/alecthomas/log4go" "github.com/gin-gonic/gin" "github.com/torkelo/grafana-pro/pkg/models" ) @@ -16,7 +17,7 @@ func init() { func (self *HttpServer) getDashboard(c *gin.Context) { id := c.Params.ByName("id") - dash, err := self.store.GetById(id) + dash, err := self.store.GetDashboardByTitle(id, "test") if err != nil { c.JSON(404, newErrorResponse("Dashboard not found")) return @@ -30,6 +31,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 +43,17 @@ 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) + + 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", "id": dashboard.Id}) return } } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 9f333aff8dc..82ce602c136 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -3,20 +3,39 @@ package models import ( "encoding/json" "io" + "time" ) type Dashboard struct { - Data map[string]interface{} + Id string `gorethink:"id,omitempty"` + AccountId string + LastModifiedByUserId string + LastModifiedByDate time.Time + CreatedDate time.Time + + Title string + Tags []string + Data map[string]interface{} +} + +type UserContext struct { + UserId string + AccountId string } type SearchResult struct { - Type string `json:"title"` Id string `json:"id"` Title string `json:"title"` } func NewDashboard(title string) *Dashboard { dash := &Dashboard{} + dash.Id = "" + dash.AccountId = "test" + dash.LastModifiedByDate = time.Now() + dash.CreatedDate = time.Now() + dash.LastModifiedByUserId = "123" + dash.Title = title dash.Data = make(map[string]interface{}) dash.Data["title"] = title @@ -31,23 +50,11 @@ func NewFromJson(reader io.Reader) (*Dashboard, error) { return nil, err } + dash.Title = dash.Data["title"].(string) + 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") -} 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..ad0f277501f --- /dev/null +++ b/pkg/stores/rethinkdb.go @@ -0,0 +1,94 @@ +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 +} + +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).Table("dashboards").IndexCreateFunc("AccountIdTitle", func(row r.Term) interface{} { + return []interface{}{row.Field("AccountId"), row.Field("Title")} + }).Exec(session) + + return &rethinkStore{ + session: session, + } +} + +func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error { + resp, err := r.Table("dashboards").Insert(dash).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) GetDashboardByTitle(title string, accountId string) (*models.Dashboard, error) { + resp, err := r.Table("dashboards").GetAllByIndex("AccountIdTitle", []interface{}{accountId, title}).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) { + log.Info("title: ", dashboard.Title) + results = append(results, &models.SearchResult{ + Title: dashboard.Title, + Id: dashboard.Id, + }) + } + + return results, nil +} + +func (self *rethinkStore) Close() {} diff --git a/pkg/stores/rethinkdb_test.go b/pkg/stores/rethinkdb_test.go new file mode 100644 index 00000000000..aa6e7d99684 --- /dev/null +++ b/pkg/stores/rethinkdb_test.go @@ -0,0 +1,29 @@ +package stores + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + "github.com/torkelo/grafana-pro/pkg/models" +) + +func TestRethinkStore(t *testing.T) { + + Convey("Insert dashboard", t, func() { + store := NewRethinkStore(&RethinkCfg{DatabaseName: "tests"}) + //defer r.DbDrop("tests").Exec(store.session) + + dashboard := models.NewDashboard("test") + dashboard.AccountId = "123" + + err := store.SaveDashboard(dashboard) + So(err, ShouldBeNil) + So(dashboard.Id, ShouldNotBeEmpty) + + read, err := store.GetDashboardByTitle("test", "123") + So(err, ShouldBeNil) + So(read, ShouldNotBeNil) + + }) + +} diff --git a/pkg/stores/store.go b/pkg/stores/store.go index 8ce505c420e..63fb3ca4e29 100644 --- a/pkg/stores/store.go +++ b/pkg/stores/store.go @@ -5,12 +5,12 @@ import ( ) type Store interface { - GetById(id string) (*models.Dashboard, error) - Save(dash *models.Dashboard) error + GetDashboardByTitle(id string, accountId string) (*models.Dashboard, error) + SaveDashboard(dash *models.Dashboard) error Query(query string) ([]*models.SearchResult, 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 From c684b1ddabd17d08a5dc4d11fb8f040b1bcc3bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 22 Aug 2014 15:32:42 +0200 Subject: [PATCH 2/3] dashboard load/save work and began work on register --- grafana | 2 +- pkg/api/api_dashboard.go | 6 ++-- pkg/api/api_login.go | 1 + pkg/models/dashboard_test.go | 18 ++++++++++ pkg/models/dashboards.go | 19 +++++++--- pkg/stores/rethinkdb.go | 25 ++++++++++---- pkg/stores/rethinkdb_accounts.go | 59 ++++++++++++++++++++++++++++++++ pkg/stores/rethinkdb_test.go | 32 ++++++++++++++--- pkg/stores/store.go | 2 +- 9 files changed, 143 insertions(+), 21 deletions(-) create mode 100644 pkg/models/dashboard_test.go create mode 100644 pkg/stores/rethinkdb_accounts.go diff --git a/grafana b/grafana index 06de80f2a76..477f66f56bd 160000 --- a/grafana +++ b/grafana @@ -1 +1 @@ -Subproject commit 06de80f2a764b76df6047fc4700c74aaf5734521 +Subproject commit 477f66f56bdd7f310d439ac428cb646fbb9b1949 diff --git a/pkg/api/api_dashboard.go b/pkg/api/api_dashboard.go index 5e2ca7c96d1..b2374f2e096 100644 --- a/pkg/api/api_dashboard.go +++ b/pkg/api/api_dashboard.go @@ -17,7 +17,7 @@ func init() { func (self *HttpServer) getDashboard(c *gin.Context) { id := c.Params.ByName("id") - dash, err := self.store.GetDashboardByTitle(id, "test") + dash, err := self.store.GetDashboard(id, 1) if err != nil { c.JSON(404, newErrorResponse("Dashboard not found")) return @@ -46,6 +46,8 @@ func (self *HttpServer) postDashboard(c *gin.Context) { 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) @@ -53,7 +55,7 @@ func (self *HttpServer) postDashboard(c *gin.Context) { err := self.store.SaveDashboard(dashboard) if err == nil { - c.JSON(200, gin.H{"status": "success", "id": dashboard.Id}) + 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..c722f0cdeb2 100644 --- a/pkg/api/api_login.go +++ b/pkg/api/api_login.go @@ -5,6 +5,7 @@ import "github.com/gin-gonic/gin" func init() { addRoutes(func(self *HttpServer) { self.router.GET("/login/*_", self.index) + self.router.GET("/register/*_", self.index) self.router.POST("/login", self.loginPost) self.router.POST("/logout", self.logoutPost) }) 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 82ce602c136..a2d733d4053 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -3,12 +3,15 @@ package models import ( "encoding/json" "io" + "regexp" + "strings" "time" ) type Dashboard struct { Id string `gorethink:"id,omitempty"` - AccountId string + Slug string + AccountId int LastModifiedByUserId string LastModifiedByDate time.Time CreatedDate time.Time @@ -26,18 +29,19 @@ type UserContext struct { type SearchResult struct { Id string `json:"id"` Title string `json:"title"` + Slug string `json:"slug"` } func NewDashboard(title string) *Dashboard { dash := &Dashboard{} dash.Id = "" - dash.AccountId = "test" dash.LastModifiedByDate = time.Now() dash.CreatedDate = time.Now() dash.LastModifiedByUserId = "123" - dash.Title = title dash.Data = make(map[string]interface{}) dash.Data["title"] = title + dash.Title = title + dash.UpdateSlug() return dash } @@ -50,11 +54,16 @@ func NewFromJson(reader io.Reader) (*Dashboard, error) { return nil, err } - dash.Title = dash.Data["title"].(string) - return dash, nil } func (dash *Dashboard) GetString(prop string) string { return dash.Data[prop].(string) } + +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/rethinkdb.go b/pkg/stores/rethinkdb.go index ad0f277501f..4d9d7864f16 100644 --- a/pkg/stores/rethinkdb.go +++ b/pkg/stores/rethinkdb.go @@ -16,6 +16,11 @@ type RethinkCfg struct { DatabaseName string } +type Account struct { + Id int `gorethink:"id"` + NextDashboardId int +} + func NewRethinkStore(config *RethinkCfg) *rethinkStore { log.Info("Initializing rethink storage") @@ -32,17 +37,24 @@ func NewRethinkStore(config *RethinkCfg) *rethinkStore { r.DbCreate(config.DatabaseName).Exec(session) r.Db(config.DatabaseName).TableCreate("dashboards").Exec(session) - r.Db(config.DatabaseName).Table("dashboards").IndexCreateFunc("AccountIdTitle", func(row r.Term) interface{} { - return []interface{}{row.Field("AccountId"), row.Field("Title")} + 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) + _, 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).RunWrite(self.session) + resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Upsert: true}).RunWrite(self.session) if err != nil { return err } @@ -56,8 +68,8 @@ func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error { return nil } -func (self *rethinkStore) GetDashboardByTitle(title string, accountId string) (*models.Dashboard, error) { - resp, err := r.Table("dashboards").GetAllByIndex("AccountIdTitle", []interface{}{accountId, title}).Run(self.session) +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 } @@ -81,10 +93,9 @@ func (self *rethinkStore) Query(query string) ([]*models.SearchResult, error) { results := make([]*models.SearchResult, 0, 50) var dashboard models.Dashboard for docs.Next(&dashboard) { - log.Info("title: ", dashboard.Title) results = append(results, &models.SearchResult{ Title: dashboard.Title, - Id: dashboard.Id, + Id: dashboard.Slug, }) } diff --git a/pkg/stores/rethinkdb_accounts.go b/pkg/stores/rethinkdb_accounts.go new file mode 100644 index 00000000000..60905d8ca8e --- /dev/null +++ b/pkg/stores/rethinkdb_accounts.go @@ -0,0 +1,59 @@ +package stores + +import ( + "errors" + + r "github.com/dancannon/gorethink" +) + +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) createAccount() (*Account, error) { + accountId, err := self.getNextAccountId() + if err != nil { + return nil, err + } + + account := &Account{Id: accountId, NextDashboardId: 0} + + resp, err := r.Table("accounts").Insert(account).RunWrite(self.session) + if err != nil { + return nil, err + } + + if resp.Inserted == 0 { + return nil, errors.New("Failed to insert acccount") + } + + 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 index aa6e7d99684..b7766671e4b 100644 --- a/pkg/stores/rethinkdb_test.go +++ b/pkg/stores/rethinkdb_test.go @@ -3,27 +3,49 @@ 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() { - store := NewRethinkStore(&RethinkCfg{DatabaseName: "tests"}) - //defer r.DbDrop("tests").Exec(store.session) - dashboard := models.NewDashboard("test") - dashboard.AccountId = "123" + dashboard.AccountId = 1 err := store.SaveDashboard(dashboard) So(err, ShouldBeNil) So(dashboard.Id, ShouldNotBeEmpty) - read, err := store.GetDashboardByTitle("test", "123") + 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, err := store.createAccount() + So(err, ShouldBeNil) + So(account, ShouldNotBeNil) + So(account.Id, ShouldNotEqual, 0) + }) + + Convey("can get next dashboard id", t, func() { + account, err := store.createAccount() + dashId, err := store.getNextDashboardNumber(account.Id) + So(err, ShouldBeNil) + So(dashId, ShouldEqual, 1) }) } diff --git a/pkg/stores/store.go b/pkg/stores/store.go index 63fb3ca4e29..7b233e72294 100644 --- a/pkg/stores/store.go +++ b/pkg/stores/store.go @@ -5,7 +5,7 @@ import ( ) type Store interface { - GetDashboardByTitle(id string, accountId string) (*models.Dashboard, error) + GetDashboard(title string, accountId int) (*models.Dashboard, error) SaveDashboard(dash *models.Dashboard) error Query(query string) ([]*models.SearchResult, error) Close() From 31fe471da55771fb3ac2562ab713b0a5a9eb68dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 22 Aug 2014 18:05:37 +0200 Subject: [PATCH 3/3] working on account registration and more --- pkg/api/api.go | 4 +-- pkg/api/api_dashboard.go | 9 ++++--- pkg/api/api_login.go | 38 ++++++++++++++++++--------- pkg/api/api_register.go | 45 ++++++++++++++++++++++++++++++++ pkg/models/dashboards.go | 20 ++++++++++++++ pkg/stores/rethinkdb.go | 5 ++++ pkg/stores/rethinkdb_accounts.go | 29 +++++++++++++++----- pkg/stores/rethinkdb_test.go | 15 +++++++---- pkg/stores/store.go | 2 ++ 9 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 pkg/api/api_register.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 b2374f2e096..208b25c7b1f 100644 --- a/pkg/api/api_dashboard.go +++ b/pkg/api/api_dashboard.go @@ -8,16 +8,17 @@ import ( 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.GetDashboard(id, 1) + dash, err := self.store.GetDashboard(id, accountId.(int)) if err != nil { c.JSON(404, newErrorResponse("Dashboard not found")) return diff --git a/pkg/api/api_login.go b/pkg/api/api_login.go index c722f0cdeb2..fa493ba65b9 100644 --- a/pkg/api/api_login.go +++ b/pkg/api/api_login.go @@ -5,7 +5,6 @@ import "github.com/gin-gonic/gin" func init() { addRoutes(func(self *HttpServer) { self.router.GET("/login/*_", self.index) - self.router.GET("/register/*_", self.index) self.router.POST("/login", self.loginPost) self.router.POST("/logout", self.logoutPost) }) @@ -20,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) { @@ -42,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/dashboards.go b/pkg/models/dashboards.go index a2d733d4053..53bd41d6bf8 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -21,6 +21,26 @@ type Dashboard struct { 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 diff --git a/pkg/stores/rethinkdb.go b/pkg/stores/rethinkdb.go index 4d9d7864f16..abb974fdd29 100644 --- a/pkg/stores/rethinkdb.go +++ b/pkg/stores/rethinkdb.go @@ -39,10 +39,15 @@ func NewRethinkStore(config *RethinkCfg) *rethinkStore { 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) diff --git a/pkg/stores/rethinkdb_accounts.go b/pkg/stores/rethinkdb_accounts.go index 60905d8ca8e..e2a337146ae 100644 --- a/pkg/stores/rethinkdb_accounts.go +++ b/pkg/stores/rethinkdb_accounts.go @@ -4,6 +4,7 @@ import ( "errors" r "github.com/dancannon/gorethink" + "github.com/torkelo/grafana-pro/pkg/models" ) func (self *rethinkStore) getNextAccountId() (int, error) { @@ -22,24 +23,40 @@ func (self *rethinkStore) getNextAccountId() (int, error) { return int(resp.NewValue.(map[string]interface{})["NextAccountId"].(float64)), nil } -func (self *rethinkStore) createAccount() (*Account, error) { +func (self *rethinkStore) SaveUserAccount(account *models.UserAccount) error { accountId, err := self.getNextAccountId() if err != nil { - return nil, err + return err } - account := &Account{Id: accountId, NextDashboardId: 0} + account.DatabaseId = accountId resp, err := r.Table("accounts").Insert(account).RunWrite(self.session) if err != nil { - return nil, err + return err } if resp.Inserted == 0 { - return nil, errors.New("Failed to insert acccount") + return errors.New("Failed to insert acccount") } - return account, nil + 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) { diff --git a/pkg/stores/rethinkdb_test.go b/pkg/stores/rethinkdb_test.go index b7766671e4b..e90abb785e8 100644 --- a/pkg/stores/rethinkdb_test.go +++ b/pkg/stores/rethinkdb_test.go @@ -35,15 +35,20 @@ func TestRethinkStore(t *testing.T) { }) Convey("can create account", t, func() { - account, err := store.createAccount() + account := &models.UserAccount{UserName: "torkelo", Email: "mupp", Login: "test@test.com"} + err := store.SaveUserAccount(account) So(err, ShouldBeNil) - So(account, ShouldNotBeNil) - So(account.Id, ShouldNotEqual, 0) + 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, err := store.createAccount() - dashId, err := store.getNextDashboardNumber(account.Id) + 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 7b233e72294..8319e7ca86f 100644 --- a/pkg/stores/store.go +++ b/pkg/stores/store.go @@ -8,6 +8,8 @@ type Store interface { 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() }