Browse Source

Merge pull request #10865 from grafana/provisioning

Support deleting provisioned dashboards when file is removed
Leonard Gram 8 years ago
parent
commit
5af2d09fb3

+ 1 - 1
conf/provisioning/dashboards/sample.yaml

@@ -3,4 +3,4 @@
 #   folder: ''
 #   type: file
 #   options:
-#     folder: /var/lib/grafana/dashboards
+#     path: /var/lib/grafana/dashboards

+ 1 - 1
pkg/api/dashboard.go

@@ -232,7 +232,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
 		}
 	}
 
-	dashItem := &dashboards.SaveDashboardItem{
+	dashItem := &dashboards.SaveDashboardDTO{
 		Dashboard: dash,
 		Message:   cmd.Message,
 		OrgId:     c.OrgId,

+ 12 - 2
pkg/api/dashboard_test.go

@@ -17,15 +17,25 @@ import (
 )
 
 type fakeDashboardRepo struct {
-	inserted     []*dashboards.SaveDashboardItem
+	inserted     []*dashboards.SaveDashboardDTO
+	provisioned  []*m.DashboardProvisioning
 	getDashboard []*m.Dashboard
 }
 
-func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*m.Dashboard, error) {
+func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardDTO) (*m.Dashboard, error) {
 	repo.inserted = append(repo.inserted, json)
 	return json.Dashboard, nil
 }
 
+func (repo *fakeDashboardRepo) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *m.DashboardProvisioning) (*m.Dashboard, error) {
+	repo.inserted = append(repo.inserted, dto)
+	return dto.Dashboard, nil
+}
+
+func (repo *fakeDashboardRepo) GetProvisionedDashboardData(name string) ([]*m.DashboardProvisioning, error) {
+	return repo.provisioned, nil
+}
+
 var fakeRepo *fakeDashboardRepo
 
 // This tests two main scenarios. If a user has access to execute an action on a dashboard:

+ 26 - 0
pkg/models/dashboards.go

@@ -69,6 +69,11 @@ type Dashboard struct {
 	Data  *simplejson.Json
 }
 
+func (d *Dashboard) SetId(id int64) {
+	d.Id = id
+	d.Data.Set("id", id)
+}
+
 // NewDashboard creates a new dashboard
 func NewDashboard(title string) *Dashboard {
 	dash := &Dashboard{}
@@ -219,6 +224,21 @@ type SaveDashboardCommand struct {
 	Result *Dashboard
 }
 
+type DashboardProvisioning struct {
+	Id          int64
+	DashboardId int64
+	Name        string
+	ExternalId  string
+	Updated     time.Time
+}
+
+type SaveProvisionedDashboardCommand struct {
+	DashboardCmd          *SaveDashboardCommand
+	DashboardProvisioning *DashboardProvisioning
+
+	Result *Dashboard
+}
+
 type DeleteDashboardCommand struct {
 	Id    int64
 	OrgId int64
@@ -271,6 +291,12 @@ type GetDashboardSlugByIdQuery struct {
 	Result string
 }
 
+type GetProvisionedDashboardDataQuery struct {
+	Name string
+
+	Result []*DashboardProvisioning
+}
+
 type GetDashboardsBySlugQuery struct {
 	OrgId int64
 	Slug  string

+ 75 - 19
pkg/services/dashboards/dashboards.go

@@ -9,7 +9,9 @@ import (
 )
 
 type Repository interface {
-	SaveDashboard(*SaveDashboardItem) (*models.Dashboard, error)
+	SaveDashboard(*SaveDashboardDTO) (*models.Dashboard, error)
+	SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
+	GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
 }
 
 var repositoryInstance Repository
@@ -22,7 +24,7 @@ func SetRepository(rep Repository) {
 	repositoryInstance = rep
 }
 
-type SaveDashboardItem struct {
+type SaveDashboardDTO struct {
 	OrgId     int64
 	UpdatedAt time.Time
 	UserId    int64
@@ -33,15 +35,25 @@ type SaveDashboardItem struct {
 
 type DashboardRepository struct{}
 
-func (dr *DashboardRepository) SaveDashboard(json *SaveDashboardItem) (*models.Dashboard, error) {
-	dashboard := json.Dashboard
+func (dr *DashboardRepository) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
+	cmd := &models.GetProvisionedDashboardDataQuery{Name: name}
+	err := bus.Dispatch(cmd)
+	if err != nil {
+		return nil, err
+	}
+
+	return cmd.Result, nil
+}
+
+func (dr *DashboardRepository) buildSaveDashboardCommand(dto *SaveDashboardDTO) (*models.SaveDashboardCommand, error) {
+	dashboard := dto.Dashboard
 
 	if dashboard.Title == "" {
 		return nil, models.ErrDashboardTitleEmpty
 	}
 
 	validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{
-		OrgId:     json.OrgId,
+		OrgId:     dto.OrgId,
 		Dashboard: dashboard,
 	}
 
@@ -49,33 +61,77 @@ func (dr *DashboardRepository) SaveDashboard(json *SaveDashboardItem) (*models.D
 		return nil, models.ErrDashboardContainsInvalidAlertData
 	}
 
-	cmd := models.SaveDashboardCommand{
+	cmd := &models.SaveDashboardCommand{
 		Dashboard: dashboard.Data,
-		Message:   json.Message,
-		OrgId:     json.OrgId,
-		Overwrite: json.Overwrite,
-		UserId:    json.UserId,
+		Message:   dto.Message,
+		OrgId:     dto.OrgId,
+		Overwrite: dto.Overwrite,
+		UserId:    dto.UserId,
 		FolderId:  dashboard.FolderId,
 		IsFolder:  dashboard.IsFolder,
 	}
 
-	if !json.UpdatedAt.IsZero() {
-		cmd.UpdatedAt = json.UpdatedAt
+	if !dto.UpdatedAt.IsZero() {
+		cmd.UpdatedAt = dto.UpdatedAt
 	}
 
-	err := bus.Dispatch(&cmd)
-	if err != nil {
-		return nil, err
-	}
+	return cmd, nil
+}
 
+func (dr *DashboardRepository) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
 	alertCmd := alerting.UpdateDashboardAlertsCommand{
-		OrgId:     json.OrgId,
-		UserId:    json.UserId,
+		OrgId:     dto.OrgId,
+		UserId:    dto.UserId,
 		Dashboard: cmd.Result,
 	}
 
 	if err := bus.Dispatch(&alertCmd); err != nil {
-		return nil, models.ErrDashboardFailedToUpdateAlertData
+		return models.ErrDashboardFailedToUpdateAlertData
+	}
+
+	return nil
+}
+
+func (dr *DashboardRepository) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
+	cmd, err := dr.buildSaveDashboardCommand(dto)
+	if err != nil {
+		return nil, err
+	}
+
+	saveCmd := &models.SaveProvisionedDashboardCommand{
+		DashboardCmd:          cmd,
+		DashboardProvisioning: provisioning,
+	}
+
+	// dashboard
+	err = bus.Dispatch(saveCmd)
+	if err != nil {
+		return nil, err
+	}
+
+	//alerts
+	err = dr.updateAlerting(cmd, dto)
+	if err != nil {
+		return nil, err
+	}
+
+	return cmd.Result, nil
+}
+
+func (dr *DashboardRepository) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
+	cmd, err := dr.buildSaveDashboardCommand(dto)
+	if err != nil {
+		return nil, err
+	}
+
+	err = bus.Dispatch(cmd)
+	if err != nil {
+		return nil, err
+	}
+
+	err = dr.updateAlerting(cmd, dto)
+	if err != nil {
+		return nil, err
 	}
 
 	return cmd.Result, nil

+ 0 - 33
pkg/services/provisioning/dashboards/dashboard_cache.go

@@ -1,33 +0,0 @@
-package dashboards
-
-import (
-	"github.com/grafana/grafana/pkg/services/dashboards"
-	gocache "github.com/patrickmn/go-cache"
-	"time"
-)
-
-type dashboardCache struct {
-	internalCache *gocache.Cache
-}
-
-func NewDashboardCache() *dashboardCache {
-	return &dashboardCache{internalCache: gocache.New(5*time.Minute, 30*time.Minute)}
-}
-
-func (fr *dashboardCache) addDashboardCache(key string, json *dashboards.SaveDashboardItem) {
-	fr.internalCache.Add(key, json, time.Minute*10)
-}
-
-func (fr *dashboardCache) getCache(key string) (*dashboards.SaveDashboardItem, bool) {
-	obj, exist := fr.internalCache.Get(key)
-	if !exist {
-		return nil, exist
-	}
-
-	dash, ok := obj.(*dashboards.SaveDashboardItem)
-	if !ok {
-		return nil, ok
-	}
-
-	return dash, ok
-}

+ 121 - 72
pkg/services/provisioning/dashboards/file_reader.go

@@ -29,8 +29,6 @@ type fileReader struct {
 	Path          string
 	log           log.Logger
 	dashboardRepo dashboards.Repository
-	cache         *dashboardCache
-	createWalk    func(fr *fileReader, folderId int64) filepath.WalkFunc
 }
 
 func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
@@ -54,24 +52,22 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade
 		Path:          path,
 		log:           log,
 		dashboardRepo: dashboards.GetRepository(),
-		cache:         NewDashboardCache(),
-		createWalk:    createWalkFn,
 	}, nil
 }
 
 func (fr *fileReader) ReadAndListen(ctx context.Context) error {
-	ticker := time.NewTicker(checkDiskForChangesInterval)
-
 	if err := fr.startWalkingDisk(); err != nil {
 		fr.log.Error("failed to search for dashboards", "error", err)
 	}
 
+	ticker := time.NewTicker(checkDiskForChangesInterval)
+
 	running := false
 
 	for {
 		select {
 		case <-ticker.C:
-			if !running { // avoid walking the filesystem in parallel. incase fs is very slow.
+			if !running { // avoid walking the filesystem in parallel. in-case fs is very slow.
 				running = true
 				go func() {
 					if err := fr.startWalkingDisk(); err != nil {
@@ -98,7 +94,91 @@ func (fr *fileReader) startWalkingDisk() error {
 		return err
 	}
 
-	return filepath.Walk(fr.Path, fr.createWalk(fr, folderId))
+	provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardRepo, fr.Cfg.Name)
+	if err != nil {
+		return err
+	}
+
+	filesFoundOnDisk := map[string]os.FileInfo{}
+	err = filepath.Walk(fr.Path, createWalkFn(filesFoundOnDisk))
+	if err != nil {
+		return err
+	}
+
+	// find dashboards to delete since json file is missing
+	var dashboardToDelete []int64
+	for path, provisioningData := range provisionedDashboardRefs {
+		_, existsOnDisk := filesFoundOnDisk[path]
+		if !existsOnDisk {
+			dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId)
+		}
+	}
+
+	// delete dashboard that are missing json file
+	for _, dashboardId := range dashboardToDelete {
+		fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId)
+		cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId}
+		err := bus.Dispatch(cmd)
+		if err != nil {
+			fr.log.Error("failed to delete dashboard", "id", cmd.Id)
+		}
+	}
+
+	// save dashboards based on json files
+	for path, fileInfo := range filesFoundOnDisk {
+		err = fr.saveDashboard(path, folderId, fileInfo, provisionedDashboardRefs)
+		if err != nil {
+			fr.log.Error("failed to save dashboard", "error", err)
+		}
+	}
+
+	return nil
+}
+
+func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) error {
+	resolvedFileInfo, err := resolveSymlink(fileInfo, path)
+	if err != nil {
+		return err
+	}
+
+	provisionedData, alreadyProvisioned := provisionedDashboardRefs[path]
+	if alreadyProvisioned && provisionedData.Updated.Unix() == resolvedFileInfo.ModTime().Unix() {
+		return nil // dashboard is already in sync with the database
+	}
+
+	dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId)
+	if err != nil {
+		fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
+		return nil
+	}
+
+	if dash.Dashboard.Id != 0 {
+		fr.log.Error("provisioned dashboard json files cannot contain id")
+		return nil
+	}
+
+	if alreadyProvisioned {
+		dash.Dashboard.SetId(provisionedData.DashboardId)
+	}
+
+	fr.log.Debug("saving new dashboard", "file", path)
+	dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime()}
+	_, err = fr.dashboardRepo.SaveProvisionedDashboard(dash, dp)
+	return err
+}
+
+func getProvisionedDashboardByPath(repo dashboards.Repository, name string) (map[string]*models.DashboardProvisioning, error) {
+	arr, err := repo.GetProvisionedDashboardData(name)
+	if err != nil {
+		return nil, err
+	}
+
+	byPath := map[string]*models.DashboardProvisioning{}
+	for _, pd := range arr {
+		byPath[pd.ExternalId] = pd
+	}
+
+	return byPath, nil
 }
 
 func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (int64, error) {
@@ -115,7 +195,7 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (i
 
 	// dashboard folder not found. create one.
 	if err == models.ErrDashboardNotFound {
-		dash := &dashboards.SaveDashboardItem{}
+		dash := &dashboards.SaveDashboardDTO{}
 		dash.Dashboard = models.NewDashboard(cfg.Folder)
 		dash.Dashboard.IsFolder = true
 		dash.Overwrite = true
@@ -129,83 +209,59 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (i
 	}
 
 	if !cmd.Result.IsFolder {
-		return 0, fmt.Errorf("Got invalid response. Expected folder, found dashboard")
+		return 0, fmt.Errorf("got invalid response. expected folder, found dashboard")
 	}
 
 	return cmd.Result.Id, nil
 }
 
-func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc {
-	return func(path string, fileInfo os.FileInfo, err error) error {
+func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) {
+	checkFilepath, err := filepath.EvalSymlinks(path)
+	if path != checkFilepath {
+		path = checkFilepath
+		fi, err := os.Lstat(checkFilepath)
 		if err != nil {
-			return err
-		}
-		if fileInfo.IsDir() {
-			if strings.HasPrefix(fileInfo.Name(), ".") {
-				return filepath.SkipDir
-			}
-			return nil
+			return nil, err
 		}
 
-		if !strings.HasSuffix(fileInfo.Name(), ".json") {
-			return nil
-		}
-
-		checkFilepath, err := filepath.EvalSymlinks(path)
-
-		if path != checkFilepath {
-			path = checkFilepath
-			fi, err := os.Lstat(checkFilepath)
-			if err != nil {
-				return err
-			}
-			fileInfo = fi
-		}
+		return fi, nil
+	}
 
-		cachedDashboard, exist := fr.cache.getCache(path)
-		if exist && cachedDashboard.UpdatedAt == fileInfo.ModTime() {
-			return nil
-		}
+	return fileinfo, err
+}
 
-		dash, err := fr.readDashboardFromFile(path, folderId)
+func createWalkFn(filesOnDisk map[string]os.FileInfo) filepath.WalkFunc {
+	return func(path string, fileInfo os.FileInfo, err error) error {
 		if err != nil {
-			fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
-			return nil
-		}
-
-		if dash.Dashboard.Id != 0 {
-			fr.log.Error("Cannot provision dashboard. Please remove the id property from the json file")
-			return nil
+			return err
 		}
 
-		cmd := &models.GetDashboardQuery{Slug: dash.Dashboard.Slug}
-		err = bus.Dispatch(cmd)
-
-		// if we don't have the dashboard in the db, save it!
-		if err == models.ErrDashboardNotFound {
-			fr.log.Debug("saving new dashboard", "file", path)
-			_, err = fr.dashboardRepo.SaveDashboard(dash)
+		isValid, err := validateWalkablePath(fileInfo)
+		if !isValid {
 			return err
 		}
 
-		if err != nil {
-			fr.log.Error("failed to query for dashboard", "slug", dash.Dashboard.Slug, "error", err)
-			return nil
-		}
+		filesOnDisk[path] = fileInfo
+		return nil
+	}
+}
 
-		// break if db version is newer then fil version
-		if cmd.Result.Updated.Unix() >= fileInfo.ModTime().Unix() {
-			return nil
+func validateWalkablePath(fileInfo os.FileInfo) (bool, error) {
+	if fileInfo.IsDir() {
+		if strings.HasPrefix(fileInfo.Name(), ".") {
+			return false, filepath.SkipDir
 		}
+		return false, nil
+	}
 
-		fr.log.Debug("loading dashboard from disk into database.", "file", path)
-		_, err = fr.dashboardRepo.SaveDashboard(dash)
-
-		return err
+	if !strings.HasSuffix(fileInfo.Name(), ".json") {
+		return false, nil
 	}
+
+	return true, nil
 }
 
-func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashboards.SaveDashboardItem, error) {
+func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboards.SaveDashboardDTO, error) {
 	reader, err := os.Open(path)
 	if err != nil {
 		return nil, err
@@ -217,17 +273,10 @@ func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashb
 		return nil, err
 	}
 
-	stat, err := os.Stat(path)
+	dash, err := createDashboardJson(data, lastModified, fr.Cfg, folderId)
 	if err != nil {
 		return nil, err
 	}
 
-	dash, err := createDashboardJson(data, stat.ModTime(), fr.Cfg, folderId)
-	if err != nil {
-		return nil, err
-	}
-
-	fr.cache.addDashboardCache(path, dash)
-
 	return dash, nil
 }

+ 17 - 34
pkg/services/provisioning/dashboards/file_reader_test.go

@@ -62,25 +62,8 @@ func TestDashboardFileReader(t *testing.T) {
 					}
 				}
 
-				So(dashboards, ShouldEqual, 2)
 				So(folders, ShouldEqual, 1)
-			})
-
-			Convey("Should not update dashboards when db is newer", func() {
-				cfg.Options["path"] = oneDashboard
-
-				fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{
-					Updated: time.Now().Add(time.Hour),
-					Slug:    "grafana",
-				})
-
-				reader, err := NewDashboardFileReader(cfg, logger)
-				So(err, ShouldBeNil)
-
-				err = reader.startWalkingDisk()
-				So(err, ShouldBeNil)
-
-				So(len(fakeRepo.inserted), ShouldEqual, 0)
+				So(dashboards, ShouldEqual, 2)
 			})
 
 			Convey("Can read default dashboard and replace old version in database", func() {
@@ -161,26 +144,15 @@ func TestDashboardFileReader(t *testing.T) {
 		})
 
 		Convey("Walking the folder with dashboards", func() {
-			cfg := &DashboardsAsConfig{
-				Name:   "Default",
-				Type:   "file",
-				OrgId:  1,
-				Folder: "",
-				Options: map[string]interface{}{
-					"path": defaultDashboards,
-				},
-			}
-
-			reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
-			So(err, ShouldBeNil)
+			noFiles := map[string]os.FileInfo{}
 
 			Convey("should skip dirs that starts with .", func() {
-				shouldSkip := reader.createWalk(reader, 0)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil)
+				shouldSkip := createWalkFn(noFiles)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil)
 				So(shouldSkip, ShouldEqual, filepath.SkipDir)
 			})
 
 			Convey("should keep walking if file is not .json", func() {
-				shouldSkip := reader.createWalk(reader, 0)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil)
+				shouldSkip := createWalkFn(noFiles)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil)
 				So(shouldSkip, ShouldBeNil)
 			})
 		})
@@ -241,15 +213,26 @@ func (ffi FakeFileInfo) Sys() interface{} {
 }
 
 type fakeDashboardRepo struct {
-	inserted     []*dashboards.SaveDashboardItem
+	inserted     []*dashboards.SaveDashboardDTO
+	provisioned  []*models.DashboardProvisioning
 	getDashboard []*models.Dashboard
 }
 
-func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*models.Dashboard, error) {
+func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardDTO) (*models.Dashboard, error) {
 	repo.inserted = append(repo.inserted, json)
 	return json.Dashboard, nil
 }
 
+func (repo *fakeDashboardRepo) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
+	return repo.provisioned, nil
+}
+
+func (repo *fakeDashboardRepo) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
+	repo.inserted = append(repo.inserted, dto)
+	repo.provisioned = append(repo.provisioned, provisioning)
+	return dto.Dashboard, nil
+}
+
 func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
 	for _, d := range fakeRepo.getDashboard {
 		if d.Slug == cmd.Slug {

+ 1 - 2
pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard1.json

@@ -1,5 +1,5 @@
 {
-    "title": "Grafana",
+    "title": "Grafana1",
     "tags": [],
     "style": "dark",
     "timezone": "browser",
@@ -170,4 +170,3 @@
     },
     "version": 5
   }
-  

+ 1 - 2
pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard2.json

@@ -1,5 +1,5 @@
 {
-    "title": "Grafana",
+    "title": "Grafana2",
     "tags": [],
     "style": "dark",
     "timezone": "browser",
@@ -170,4 +170,3 @@
     },
     "version": 5
   }
-  

+ 2 - 2
pkg/services/provisioning/dashboards/types.go

@@ -18,8 +18,8 @@ type DashboardsAsConfig struct {
 	Options  map[string]interface{} `json:"options" yaml:"options"`
 }
 
-func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardItem, error) {
-	dash := &dashboards.SaveDashboardItem{}
+func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) {
+	dash := &dashboards.SaveDashboardDTO{}
 	dash.Dashboard = models.NewDashboardFromJson(data)
 	dash.UpdatedAt = lastModified
 	dash.Overwrite = true

+ 92 - 86
pkg/services/sqlstore/dashboard.go

@@ -30,120 +30,125 @@ var generateNewUid func() string = util.GenerateShortUid
 
 func SaveDashboard(cmd *m.SaveDashboardCommand) error {
 	return inTransaction(func(sess *DBSession) error {
-		dash := cmd.GetDashboardModel()
-
-		if err := getExistingDashboardForUpdate(sess, dash, cmd); err != nil {
-			return err
-		}
+		return saveDashboard(sess, cmd)
+	})
+}
 
-		var existingByTitleAndFolder m.Dashboard
+func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
+	dash := cmd.GetDashboardModel()
 
-		dashWithTitleAndFolderExists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, dialect.BooleanStr(true), dash.FolderId).Get(&existingByTitleAndFolder)
-		if err != nil {
-			return err
-		}
+	if err := getExistingDashboardForUpdate(sess, dash, cmd); err != nil {
+		return err
+	}
 
-		if dashWithTitleAndFolderExists {
-			if dash.Id != existingByTitleAndFolder.Id {
-				if existingByTitleAndFolder.IsFolder && !cmd.IsFolder {
-					return m.ErrDashboardWithSameNameAsFolder
-				}
+	var existingByTitleAndFolder m.Dashboard
 
-				if !existingByTitleAndFolder.IsFolder && cmd.IsFolder {
-					return m.ErrDashboardFolderWithSameNameAsDashboard
-				}
+	dashWithTitleAndFolderExists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, dialect.BooleanStr(true), dash.FolderId).Get(&existingByTitleAndFolder)
+	if err != nil {
+		return err
+	}
 
-				if cmd.Overwrite {
-					dash.Id = existingByTitleAndFolder.Id
-					dash.Version = existingByTitleAndFolder.Version
+	if dashWithTitleAndFolderExists {
+		if dash.Id != existingByTitleAndFolder.Id {
+			if existingByTitleAndFolder.IsFolder && !cmd.IsFolder {
+				return m.ErrDashboardWithSameNameAsFolder
+			}
 
-					if dash.Uid == "" {
-						dash.Uid = existingByTitleAndFolder.Uid
-					}
-				} else {
-					return m.ErrDashboardWithSameNameInFolderExists
-				}
+			if !existingByTitleAndFolder.IsFolder && cmd.IsFolder {
+				return m.ErrDashboardFolderWithSameNameAsDashboard
 			}
-		}
 
-		if dash.Uid == "" {
-			uid, err := generateNewDashboardUid(sess, dash.OrgId)
-			if err != nil {
-				return err
+			if cmd.Overwrite {
+				dash.Id = existingByTitleAndFolder.Id
+				dash.Version = existingByTitleAndFolder.Version
+
+				if dash.Uid == "" {
+					dash.Uid = existingByTitleAndFolder.Uid
+				}
+			} else {
+				return m.ErrDashboardWithSameNameInFolderExists
 			}
-			dash.Uid = uid
-			dash.Data.Set("uid", uid)
 		}
+	}
 
-		err = setHasAcl(sess, dash)
+	if dash.Uid == "" {
+		uid, err := generateNewDashboardUid(sess, dash.OrgId)
 		if err != nil {
 			return err
 		}
+		dash.Uid = uid
+		dash.Data.Set("uid", uid)
+	}
 
-		parentVersion := dash.Version
-		affectedRows := int64(0)
+	err = setHasAcl(sess, dash)
+	if err != nil {
+		return err
+	}
 
-		if dash.Id == 0 {
-			dash.Version = 1
-			metrics.M_Api_Dashboard_Insert.Inc()
-			dash.Data.Set("version", dash.Version)
-			affectedRows, err = sess.Insert(dash)
-		} else {
-			dash.Version++
-			dash.Data.Set("version", dash.Version)
+	parentVersion := dash.Version
+	affectedRows := int64(0)
 
-			if !cmd.UpdatedAt.IsZero() {
-				dash.Updated = cmd.UpdatedAt
-			}
+	if dash.Id == 0 {
+		dash.Version = 1
+		metrics.M_Api_Dashboard_Insert.Inc()
+		dash.Data.Set("version", dash.Version)
+		affectedRows, err = sess.Insert(dash)
+	} else {
+		dash.Version++
+		dash.Data.Set("version", dash.Version)
 
-			affectedRows, err = sess.MustCols("folder_id", "has_acl").ID(dash.Id).Update(dash)
+		if !cmd.UpdatedAt.IsZero() {
+			dash.Updated = cmd.UpdatedAt
 		}
 
-		if err != nil {
-			return err
-		}
+		affectedRows, err = sess.MustCols("folder_id", "has_acl").ID(dash.Id).Update(dash)
+	}
 
-		if affectedRows == 0 {
-			return m.ErrDashboardNotFound
-		}
+	if err != nil {
+		return err
+	}
 
-		dashVersion := &m.DashboardVersion{
-			DashboardId:   dash.Id,
-			ParentVersion: parentVersion,
-			RestoredFrom:  cmd.RestoredFrom,
-			Version:       dash.Version,
-			Created:       time.Now(),
-			CreatedBy:     dash.UpdatedBy,
-			Message:       cmd.Message,
-			Data:          dash.Data,
-		}
+	if affectedRows == 0 {
+		return m.ErrDashboardNotFound
+	}
 
-		// insert version entry
-		if affectedRows, err = sess.Insert(dashVersion); err != nil {
-			return err
-		} else if affectedRows == 0 {
-			return m.ErrDashboardNotFound
-		}
+	dashVersion := &m.DashboardVersion{
+		DashboardId:   dash.Id,
+		ParentVersion: parentVersion,
+		RestoredFrom:  cmd.RestoredFrom,
+		Version:       dash.Version,
+		Created:       time.Now(),
+		CreatedBy:     dash.UpdatedBy,
+		Message:       cmd.Message,
+		Data:          dash.Data,
+	}
 
-		// delete existing tags
-		_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
-		if err != nil {
-			return err
-		}
+	// insert version entry
+	if affectedRows, err = sess.Insert(dashVersion); err != nil {
+		return err
+	} else if affectedRows == 0 {
+		return m.ErrDashboardNotFound
+	}
 
-		// insert new tags
-		tags := dash.GetTags()
-		if len(tags) > 0 {
-			for _, tag := range tags {
-				if _, err := sess.Insert(&DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil {
-					return err
-				}
+	// delete existing tags
+	_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
+	if err != nil {
+		return err
+	}
+
+	// insert new tags
+	tags := dash.GetTags()
+	if len(tags) > 0 {
+		for _, tag := range tags {
+			if _, err := sess.Insert(&DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil {
+				return err
 			}
 		}
-		cmd.Result = dash
+	}
 
-		return err
-	})
+	cmd.Result = dash
+
+	return err
 }
 
 func getExistingDashboardForUpdate(sess *DBSession, dash *m.Dashboard, cmd *m.SaveDashboardCommand) (err error) {
@@ -456,6 +461,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
 			"DELETE FROM dashboard_version WHERE dashboard_id = ?",
 			"DELETE FROM dashboard WHERE folder_id = ?",
 			"DELETE FROM annotation WHERE dashboard_id = ?",
+			"DELETE FROM dashboard_provisioning WHERE dashboard_id = ?",
 		}
 
 		for _, sql := range deletes {

+ 66 - 0
pkg/services/sqlstore/dashboard_provisioning.go

@@ -0,0 +1,66 @@
+package sqlstore
+
+import (
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/models"
+)
+
+func init() {
+	bus.AddHandler("sql", GetProvisionedDashboardDataQuery)
+	bus.AddHandler("sql", SaveProvisionedDashboard)
+}
+
+type DashboardExtras struct {
+	Id          int64
+	DashboardId int64
+	Key         string
+	Value       string
+}
+
+func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error {
+	return inTransaction(func(sess *DBSession) error {
+		err := saveDashboard(sess, cmd.DashboardCmd)
+
+		if err != nil {
+			return err
+		}
+
+		cmd.Result = cmd.DashboardCmd.Result
+		if cmd.DashboardProvisioning.Updated.IsZero() {
+			cmd.DashboardProvisioning.Updated = cmd.Result.Updated
+		}
+
+		return saveProvionedData(sess, cmd.DashboardProvisioning, cmd.Result)
+	})
+}
+
+func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error {
+	result := &models.DashboardProvisioning{}
+
+	exist, err := sess.Where("dashboard_id=?", dashboard.Id).Get(result)
+	if err != nil {
+		return err
+	}
+
+	cmd.Id = result.Id
+	cmd.DashboardId = dashboard.Id
+
+	if exist {
+		_, err = sess.ID(result.Id).Update(cmd)
+	} else {
+		_, err = sess.Insert(cmd)
+	}
+
+	return err
+}
+
+func GetProvisionedDashboardDataQuery(cmd *models.GetProvisionedDashboardDataQuery) error {
+	var result []*models.DashboardProvisioning
+
+	if err := x.Where("name = ?", cmd.Name).Find(&result); err != nil {
+		return err
+	}
+
+	cmd.Result = result
+	return nil
+}

+ 50 - 0
pkg/services/sqlstore/dashboard_provisioning_test.go

@@ -0,0 +1,50 @@
+package sqlstore
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/models"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestDashboardProvisioningTest(t *testing.T) {
+	Convey("Testing Dashboard provisioning", t, func() {
+		InitTestDB(t)
+
+		saveDashboardCmd := &models.SaveDashboardCommand{
+			OrgId:    1,
+			FolderId: 0,
+			IsFolder: false,
+			Dashboard: simplejson.NewFromAny(map[string]interface{}{
+				"id":    nil,
+				"title": "test dashboard",
+			}),
+		}
+
+		Convey("Saving dashboards with extras", func() {
+			cmd := &models.SaveProvisionedDashboardCommand{
+				DashboardCmd: saveDashboardCmd,
+				DashboardProvisioning: &models.DashboardProvisioning{
+					Name:       "default",
+					ExternalId: "/var/grafana.json",
+				},
+			}
+
+			err := SaveProvisionedDashboard(cmd)
+			So(err, ShouldBeNil)
+			So(cmd.Result, ShouldNotBeNil)
+			So(cmd.Result.Id, ShouldNotEqual, 0)
+			dashId := cmd.Result.Id
+
+			Convey("Can query for provisioned dashboards", func() {
+				query := &models.GetProvisionedDashboardDataQuery{Name: "default"}
+				err := GetProvisionedDashboardDataQuery(query)
+				So(err, ShouldBeNil)
+
+				So(len(query.Result), ShouldEqual, 1)
+				So(query.Result[0].DashboardId, ShouldEqual, dashId)
+			})
+		})
+	})
+}

+ 16 - 0
pkg/services/sqlstore/migrations/dashboard_mig.go

@@ -176,4 +176,20 @@ func addDashboardMigration(mg *Migrator) {
 		Cols: []string{"org_id", "folder_id", "title"}, Type: UniqueIndex,
 	}))
 
+	dashboardExtrasTable := Table{
+		Name: "dashboard_provisioning",
+		Columns: []*Column{
+			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+			{Name: "dashboard_id", Type: DB_BigInt, Nullable: true},
+			{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
+			{Name: "external_id", Type: DB_Text, Nullable: false},
+			{Name: "updated", Type: DB_DateTime, Nullable: false},
+		},
+		Indices: []*Index{
+			{Cols: []string{"dashboard_id"}},
+			{Cols: []string{"dashboard_id", "name"}, Type: IndexType},
+		},
+	}
+
+	mg.AddMigration("create dashboard_provisioning", NewAddTableMigration(dashboardExtrasTable))
 }

+ 0 - 4
pkg/services/sqlstore/playlist.go

@@ -1,8 +1,6 @@
 package sqlstore
 
 import (
-	"fmt"
-
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 )
@@ -25,8 +23,6 @@ func CreatePlaylist(cmd *m.CreatePlaylistCommand) error {
 
 	_, err := x.Insert(&playlist)
 
-	fmt.Printf("%v", playlist.Id)
-
 	playlistItems := make([]m.PlaylistItem, 0)
 	for _, item := range cmd.Items {
 		playlistItems = append(playlistItems, m.PlaylistItem{