Sfoglia il codice sorgente

Merge pull request #12122 from grafana/provisioning_ha

Support provisioning in HA setup where modtime differs
Carl Bergquist 7 anni fa
parent
commit
49d9235433

+ 1 - 1
devenv/dashboards/bulk-testing/bulkdash.jsonnet

@@ -1137,4 +1137,4 @@
   "title": "Big Dashboard",
   "uid": "000000003",
   "version": 16
-}
+}

+ 2 - 2
devenv/setup.sh

@@ -8,7 +8,7 @@ bulkDashboard() {
     MAX=400
     while [  $COUNTER -lt $MAX ]; do
         jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + {  uid: 'uid-${COUNTER}',  title: 'title-${COUNTER}' }"
-        let COUNTER=COUNTER+1 
+        let COUNTER=COUNTER+1
     done
 
     ln -s -f -r ./dashboards/bulk-testing/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
@@ -58,4 +58,4 @@ main() {
 	fi
 }
 
-main "$@"
+main "$@"

+ 1 - 0
docs/sources/administration/provisioning.md

@@ -197,6 +197,7 @@ providers:
   folder: ''
   type: file
   disableDeletion: false
+  updateIntervalSeconds: 3 #how often Grafana will scan for changed dashboards
   options:
     path: /var/lib/grafana/dashboards
 ```

+ 1 - 0
pkg/models/dashboards.go

@@ -254,6 +254,7 @@ type DashboardProvisioning struct {
 	DashboardId int64
 	Name        string
 	ExternalId  string
+	CheckSum    string
 	Updated     int64
 }
 

+ 4 - 0
pkg/services/provisioning/dashboards/config_reader.go

@@ -81,6 +81,10 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
 		if dashboards[i].OrgId == 0 {
 			dashboards[i].OrgId = 1
 		}
+
+		if dashboards[i].UpdateIntervalSeconds == 0 {
+			dashboards[i].UpdateIntervalSeconds = 3
+		}
 	}
 
 	return dashboards, nil

+ 7 - 3
pkg/services/provisioning/dashboards/config_reader_test.go

@@ -22,7 +22,7 @@ func TestDashboardsAsConfig(t *testing.T) {
 			cfg, err := cfgProvider.readConfig()
 			So(err, ShouldBeNil)
 
-			validateDashboardAsConfig(cfg)
+			validateDashboardAsConfig(t, cfg)
 		})
 
 		Convey("Can read config file in version 0 format", func() {
@@ -30,7 +30,7 @@ func TestDashboardsAsConfig(t *testing.T) {
 			cfg, err := cfgProvider.readConfig()
 			So(err, ShouldBeNil)
 
-			validateDashboardAsConfig(cfg)
+			validateDashboardAsConfig(t, cfg)
 		})
 
 		Convey("Should skip invalid path", func() {
@@ -56,7 +56,9 @@ func TestDashboardsAsConfig(t *testing.T) {
 		})
 	})
 }
-func validateDashboardAsConfig(cfg []*DashboardsAsConfig) {
+func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) {
+	t.Helper()
+
 	So(len(cfg), ShouldEqual, 2)
 
 	ds := cfg[0]
@@ -68,6 +70,7 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) {
 	So(len(ds.Options), ShouldEqual, 1)
 	So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
 	So(ds.DisableDeletion, ShouldBeTrue)
+	So(ds.UpdateIntervalSeconds, ShouldEqual, 10)
 
 	ds2 := cfg[1]
 	So(ds2.Name, ShouldEqual, "default")
@@ -78,4 +81,5 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) {
 	So(len(ds2.Options), ShouldEqual, 1)
 	So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
 	So(ds2.DisableDeletion, ShouldBeFalse)
+	So(ds2.UpdateIntervalSeconds, ShouldEqual, 3)
 }

+ 40 - 11
pkg/services/provisioning/dashboards/file_reader.go

@@ -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")
 )
 
@@ -66,7 +66,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 +159,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 +190,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 +294,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 +327,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 +359,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 +373,4 @@ func (checker provisioningSanityChecker) logWarnings(log log.Logger) {
 			log.Error("the same 'title' is used more than once", "title", title, "provider", checker.provisioningProvider)
 		}
 	}
-
 }

+ 1 - 0
pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml

@@ -6,6 +6,7 @@ providers:
   folder: 'developers'
   editable: true
   disableDeletion: true
+  updateIntervalSeconds: 10
   type: file
   options:
     path: /var/lib/grafana/dashboards

+ 1 - 0
pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml

@@ -3,6 +3,7 @@
   folder: 'developers'
   editable: true
   disableDeletion: true
+  updateIntervalSeconds: 10
   type: file
   options:
     path: /var/lib/grafana/dashboards

+ 40 - 35
pkg/services/provisioning/dashboards/types.go

@@ -10,23 +10,25 @@ import (
 )
 
 type DashboardsAsConfig struct {
-	Name            string
-	Type            string
-	OrgId           int64
-	Folder          string
-	Editable        bool
-	Options         map[string]interface{}
-	DisableDeletion bool
+	Name                  string
+	Type                  string
+	OrgId                 int64
+	Folder                string
+	Editable              bool
+	Options               map[string]interface{}
+	DisableDeletion       bool
+	UpdateIntervalSeconds int64
 }
 
 type DashboardsAsConfigV0 struct {
-	Name            string                 `json:"name" yaml:"name"`
-	Type            string                 `json:"type" yaml:"type"`
-	OrgId           int64                  `json:"org_id" yaml:"org_id"`
-	Folder          string                 `json:"folder" yaml:"folder"`
-	Editable        bool                   `json:"editable" yaml:"editable"`
-	Options         map[string]interface{} `json:"options" yaml:"options"`
-	DisableDeletion bool                   `json:"disableDeletion" yaml:"disableDeletion"`
+	Name                  string                 `json:"name" yaml:"name"`
+	Type                  string                 `json:"type" yaml:"type"`
+	OrgId                 int64                  `json:"org_id" yaml:"org_id"`
+	Folder                string                 `json:"folder" yaml:"folder"`
+	Editable              bool                   `json:"editable" yaml:"editable"`
+	Options               map[string]interface{} `json:"options" yaml:"options"`
+	DisableDeletion       bool                   `json:"disableDeletion" yaml:"disableDeletion"`
+	UpdateIntervalSeconds int64                  `json:"updateIntervalSeconds" yaml:"updateIntervalSeconds"`
 }
 
 type ConfigVersion struct {
@@ -38,13 +40,14 @@ type DashboardAsConfigV1 struct {
 }
 
 type DashboardProviderConfigs struct {
-	Name            string                 `json:"name" yaml:"name"`
-	Type            string                 `json:"type" yaml:"type"`
-	OrgId           int64                  `json:"orgId" yaml:"orgId"`
-	Folder          string                 `json:"folder" yaml:"folder"`
-	Editable        bool                   `json:"editable" yaml:"editable"`
-	Options         map[string]interface{} `json:"options" yaml:"options"`
-	DisableDeletion bool                   `json:"disableDeletion" yaml:"disableDeletion"`
+	Name                  string                 `json:"name" yaml:"name"`
+	Type                  string                 `json:"type" yaml:"type"`
+	OrgId                 int64                  `json:"orgId" yaml:"orgId"`
+	Folder                string                 `json:"folder" yaml:"folder"`
+	Editable              bool                   `json:"editable" yaml:"editable"`
+	Options               map[string]interface{} `json:"options" yaml:"options"`
+	DisableDeletion       bool                   `json:"disableDeletion" yaml:"disableDeletion"`
+	UpdateIntervalSeconds int64                  `json:"updateIntervalSeconds" yaml:"updateIntervalSeconds"`
 }
 
 func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) {
@@ -68,13 +71,14 @@ func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig
 
 	for _, v := range v0 {
 		r = append(r, &DashboardsAsConfig{
-			Name:            v.Name,
-			Type:            v.Type,
-			OrgId:           v.OrgId,
-			Folder:          v.Folder,
-			Editable:        v.Editable,
-			Options:         v.Options,
-			DisableDeletion: v.DisableDeletion,
+			Name:                  v.Name,
+			Type:                  v.Type,
+			OrgId:                 v.OrgId,
+			Folder:                v.Folder,
+			Editable:              v.Editable,
+			Options:               v.Options,
+			DisableDeletion:       v.DisableDeletion,
+			UpdateIntervalSeconds: v.UpdateIntervalSeconds,
 		})
 	}
 
@@ -86,13 +90,14 @@ func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig {
 
 	for _, v := range dc.Providers {
 		r = append(r, &DashboardsAsConfig{
-			Name:            v.Name,
-			Type:            v.Type,
-			OrgId:           v.OrgId,
-			Folder:          v.Folder,
-			Editable:        v.Editable,
-			Options:         v.Options,
-			DisableDeletion: v.DisableDeletion,
+			Name:                  v.Name,
+			Type:                  v.Type,
+			OrgId:                 v.OrgId,
+			Folder:                v.Folder,
+			Editable:              v.Editable,
+			Options:               v.Options,
+			DisableDeletion:       v.DisableDeletion,
+			UpdateIntervalSeconds: v.UpdateIntervalSeconds,
 		})
 	}
 

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

@@ -211,4 +211,8 @@ func addDashboardMigration(mg *Migrator) {
 		"name":         "name",
 		"external_id":  "external_id",
 	})
+
+	mg.AddMigration("Add check_sum column", NewAddColumnMigration(dashboardExtrasTableV2, &Column{
+		Name: "check_sum", Type: DB_NVarchar, Length: 32, Nullable: true,
+	}))
 }

+ 26 - 0
pkg/util/md5.go

@@ -0,0 +1,26 @@
+package util
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"io"
+	"strings"
+)
+
+// Md5Sum calculates the md5sum of a stream
+func Md5Sum(reader io.Reader) (string, error) {
+	var returnMD5String string
+	hash := md5.New()
+	if _, err := io.Copy(hash, reader); err != nil {
+		return returnMD5String, err
+	}
+	hashInBytes := hash.Sum(nil)[:16]
+	returnMD5String = hex.EncodeToString(hashInBytes)
+	return returnMD5String, nil
+}
+
+// Md5Sum calculates the md5sum of a string
+func Md5SumString(input string) (string, error) {
+	buffer := strings.NewReader(input)
+	return Md5Sum(buffer)
+}

+ 17 - 0
pkg/util/md5_test.go

@@ -0,0 +1,17 @@
+package util
+
+import "testing"
+
+func TestMd5Sum(t *testing.T) {
+	input := "dont hash passwords with md5"
+
+	have, err := Md5SumString(input)
+	if err != nil {
+		t.Fatal("expected err to be nil")
+	}
+
+	want := "2d6a56c82d09d374643b926d3417afba"
+	if have != want {
+		t.Fatalf("expected: %s got: %s", want, have)
+	}
+}