|
|
@@ -4,12 +4,14 @@ import (
|
|
|
"context"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
+ "io/ioutil"
|
|
|
"os"
|
|
|
"path/filepath"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
|
+ "github.com/grafana/grafana/pkg/util"
|
|
|
|
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
|
|
|
|
@@ -19,8 +21,6 @@ import (
|
|
|
)
|
|
|
|
|
|
var (
|
|
|
- checkDiskForChangesInterval = time.Second * 3
|
|
|
-
|
|
|
ErrFolderNameMissing = errors.New("Folder name missing")
|
|
|
)
|
|
|
|
|
|
@@ -47,15 +47,25 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade
|
|
|
log.Error("Cannot read directory", "error", err)
|
|
|
}
|
|
|
|
|
|
- absPath, err := filepath.Abs(path)
|
|
|
+ copy := path
|
|
|
+ path, err := filepath.Abs(path)
|
|
|
if err != nil {
|
|
|
log.Error("Could not create absolute path ", "path", path)
|
|
|
- absPath = path //if .Abs return an error we fallback to path
|
|
|
+ }
|
|
|
+
|
|
|
+ path, err = filepath.EvalSymlinks(path)
|
|
|
+ if err != nil {
|
|
|
+ log.Error("Failed to read content of symlinked path: %s", path)
|
|
|
+ }
|
|
|
+
|
|
|
+ if path == "" {
|
|
|
+ path = copy
|
|
|
+ log.Info("falling back to original path due to EvalSymlink/Abs failure")
|
|
|
}
|
|
|
|
|
|
return &fileReader{
|
|
|
Cfg: cfg,
|
|
|
- Path: absPath,
|
|
|
+ Path: path,
|
|
|
log: log,
|
|
|
dashboardService: dashboards.NewProvisioningService(),
|
|
|
}, nil
|
|
|
@@ -66,7 +76,7 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error {
|
|
|
fr.log.Error("failed to search for dashboards", "error", err)
|
|
|
}
|
|
|
|
|
|
- ticker := time.NewTicker(checkDiskForChangesInterval)
|
|
|
+ ticker := time.NewTicker(time.Duration(int64(time.Second) * fr.Cfg.UpdateIntervalSeconds))
|
|
|
|
|
|
running := false
|
|
|
|
|
|
@@ -159,15 +169,20 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
|
|
|
}
|
|
|
|
|
|
provisionedData, alreadyProvisioned := provisionedDashboardRefs[path]
|
|
|
- upToDate := alreadyProvisioned && provisionedData.Updated == resolvedFileInfo.ModTime().Unix()
|
|
|
+ upToDate := alreadyProvisioned && provisionedData.Updated >= resolvedFileInfo.ModTime().Unix()
|
|
|
|
|
|
- dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId)
|
|
|
+ jsonFile, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId)
|
|
|
if err != nil {
|
|
|
fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
|
|
|
return provisioningMetadata, nil
|
|
|
}
|
|
|
|
|
|
+ if provisionedData != nil && jsonFile.checkSum == provisionedData.CheckSum {
|
|
|
+ upToDate = true
|
|
|
+ }
|
|
|
+
|
|
|
// keeps track of what uid's and title's we have already provisioned
|
|
|
+ dash := jsonFile.dashboard
|
|
|
provisioningMetadata.uid = dash.Dashboard.Uid
|
|
|
provisioningMetadata.title = dash.Dashboard.Title
|
|
|
|
|
|
@@ -185,7 +200,13 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
|
|
|
}
|
|
|
|
|
|
fr.log.Debug("saving new dashboard", "file", path)
|
|
|
- dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix()}
|
|
|
+ dp := &models.DashboardProvisioning{
|
|
|
+ ExternalId: path,
|
|
|
+ Name: fr.Cfg.Name,
|
|
|
+ Updated: resolvedFileInfo.ModTime().Unix(),
|
|
|
+ CheckSum: jsonFile.checkSum,
|
|
|
+ }
|
|
|
+
|
|
|
_, err = fr.dashboardService.SaveProvisionedDashboard(dash, dp)
|
|
|
return provisioningMetadata, err
|
|
|
}
|
|
|
@@ -283,14 +304,30 @@ func validateWalkablePath(fileInfo os.FileInfo) (bool, error) {
|
|
|
return true, nil
|
|
|
}
|
|
|
|
|
|
-func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboards.SaveDashboardDTO, error) {
|
|
|
+type dashboardJsonFile struct {
|
|
|
+ dashboard *dashboards.SaveDashboardDTO
|
|
|
+ checkSum string
|
|
|
+ lastModified time.Time
|
|
|
+}
|
|
|
+
|
|
|
+func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboardJsonFile, error) {
|
|
|
reader, err := os.Open(path)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
defer reader.Close()
|
|
|
|
|
|
- data, err := simplejson.NewFromReader(reader)
|
|
|
+ all, err := ioutil.ReadAll(reader)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ checkSum, err := util.Md5SumString(string(all))
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ data, err := simplejson.NewJson(all)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
@@ -300,7 +337,11 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time,
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
- return dash, nil
|
|
|
+ return &dashboardJsonFile{
|
|
|
+ dashboard: dash,
|
|
|
+ checkSum: checkSum,
|
|
|
+ lastModified: lastModified,
|
|
|
+ }, nil
|
|
|
}
|
|
|
|
|
|
type provisioningMetadata struct {
|
|
|
@@ -328,7 +369,6 @@ func (checker provisioningSanityChecker) track(pm provisioningMetadata) {
|
|
|
if len(pm.title) > 0 {
|
|
|
checker.titleUsage[pm.title] += 1
|
|
|
}
|
|
|
-
|
|
|
}
|
|
|
|
|
|
func (checker provisioningSanityChecker) logWarnings(log log.Logger) {
|
|
|
@@ -343,5 +383,4 @@ func (checker provisioningSanityChecker) logWarnings(log log.Logger) {
|
|
|
log.Error("the same 'title' is used more than once", "title", title, "provider", checker.provisioningProvider)
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
}
|