Merge pull request #12122 from grafana/provisioning_ha

Support provisioning in HA setup where modtime differs
This commit is contained in:
Carl Bergquist
2018-06-04 15:12:46 +02:00
committed by GitHub
13 changed files with 145 additions and 52 deletions

View File

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

View File

@@ -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 "$@"

View File

@@ -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
```

View File

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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)
}
}
}

View File

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

View File

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

View File

@@ -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,
})
}

View File

@@ -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
pkg/util/md5.go Normal file
View File

@@ -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
pkg/util/md5_test.go Normal file
View File

@@ -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)
}
}