grafana/pkg/services/provisioning/dashboards/file_reader.go
Karsten Weiss b7adf28501 Remove redundancy in variable declarations (golint)
This commit fixes the following golint warnings:
pkg/api/avatar/avatar.go:229:12: should omit type *http.Client from declaration of var client; it will be inferred from the right-hand side
pkg/login/brute_force_login_protection.go:13:26: should omit type time.Duration from declaration of var loginAttemptsWindow; it will be inferred from the right-hand side
pkg/metrics/graphitebridge/graphite.go:58:26: should omit type []string from declaration of var metricCategoryPrefix; it will be inferred from the right-hand side
pkg/metrics/graphitebridge/graphite.go:69:22: should omit type []string from declaration of var trimMetricPrefix; it will be inferred from the right-hand side
pkg/models/alert.go:37:36: should omit type error from declaration of var ErrCannotChangeStateOnPausedAlert; it will be inferred from the right-hand side
pkg/models/alert.go:38:36: should omit type error from declaration of var ErrRequiresNewState; it will be inferred from the right-hand side
pkg/models/datasource.go:61:28: should omit type map[string]bool from declaration of var knownDatasourcePlugins; it will be inferred from the right-hand side
pkg/plugins/update_checker.go:16:13: should omit type http.Client from declaration of var httpClient; it will be inferred from the right-hand side
pkg/services/alerting/engine.go:103:24: should omit type time.Duration from declaration of var unfinishedWorkTimeout; it will be inferred from the right-hand side
pkg/services/alerting/engine.go:105:19: should omit type time.Duration from declaration of var alertTimeout; it will be inferred from the right-hand side
pkg/services/alerting/engine.go:106:19: should omit type int from declaration of var alertMaxAttempts; it will be inferred from the right-hand side
pkg/services/alerting/notifier.go:143:23: should omit type map[string]*NotifierPlugin from declaration of var notifierFactories; it will be inferred from the right-hand side
pkg/services/alerting/rule.go:136:24: should omit type map[string]ConditionFactory from declaration of var conditionFactories; it will be inferred from the right-hand side
pkg/services/alerting/conditions/evaluator.go:12:15: should omit type []string from declaration of var defaultTypes; it will be inferred from the right-hand side
pkg/services/alerting/conditions/evaluator.go:13:15: should omit type []string from declaration of var rangedTypes; it will be inferred from the right-hand side
pkg/services/alerting/notifiers/opsgenie.go:44:19: should omit type string from declaration of var opsgenieAlertURL; it will be inferred from the right-hand side
pkg/services/alerting/notifiers/pagerduty.go:43:23: should omit type string from declaration of var pagerdutyEventApiUrl; it will be inferred from the right-hand side
pkg/services/alerting/notifiers/telegram.go:21:17: should omit type string from declaration of var telegramApiUrl; it will be inferred from the right-hand side
pkg/services/provisioning/dashboards/config_reader_test.go:11:24: should omit type string from declaration of var simpleDashboardConfig; it will be inferred from the right-hand side
pkg/services/provisioning/dashboards/config_reader_test.go:12:24: should omit type string from declaration of var oldVersion; it will be inferred from the right-hand side
pkg/services/provisioning/dashboards/config_reader_test.go:13:24: should omit type string from declaration of var brokenConfigs; it will be inferred from the right-hand side
pkg/services/provisioning/dashboards/file_reader.go:22:30: should omit type time.Duration from declaration of var checkDiskForChangesInterval; it will be inferred from the right-hand side
pkg/services/provisioning/dashboards/file_reader.go:24:23: should omit type error from declaration of var ErrFolderNameMissing; it will be inferred from the right-hand side
pkg/services/provisioning/datasources/config_reader_test.go:15:34: should omit type string from declaration of var twoDatasourcesConfig; it will be inferred from the right-hand side
pkg/services/provisioning/datasources/config_reader_test.go:16:34: should omit type string from declaration of var twoDatasourcesConfigPurgeOthers; it will be inferred from the right-hand side
pkg/services/provisioning/datasources/config_reader_test.go:17:34: should omit type string from declaration of var doubleDatasourcesConfig; it will be inferred from the right-hand side
pkg/services/provisioning/datasources/config_reader_test.go:18:34: should omit type string from declaration of var allProperties; it will be inferred from the right-hand side
pkg/services/provisioning/datasources/config_reader_test.go:19:34: should omit type string from declaration of var versionZero; it will be inferred from the right-hand side
pkg/services/provisioning/datasources/config_reader_test.go:20:34: should omit type string from declaration of var brokenYaml; it will be inferred from the right-hand side
pkg/services/sqlstore/stats.go:16:25: should omit type time.Duration from declaration of var activeUserTimeLimit; it will be inferred from the right-hand side
pkg/services/sqlstore/migrator/mysql_dialect.go:69:14: should omit type bool from declaration of var hasLen1; it will be inferred from the right-hand side
pkg/services/sqlstore/migrator/mysql_dialect.go:70:14: should omit type bool from declaration of var hasLen2; it will be inferred from the right-hand side
pkg/services/sqlstore/migrator/postgres_dialect.go:95:14: should omit type bool from declaration of var hasLen1; it will be inferred from the right-hand side
pkg/services/sqlstore/migrator/postgres_dialect.go:96:14: should omit type bool from declaration of var hasLen2; it will be inferred from the right-hand side
pkg/setting/setting.go:42:15: should omit type string from declaration of var Env; it will be inferred from the right-hand side
pkg/setting/setting.go:161:18: should omit type bool from declaration of var LdapAllowSignup; it will be inferred from the right-hand side
pkg/setting/setting.go:473:30: should omit type bool from declaration of var skipStaticRootValidation; it will be inferred from the right-hand side
pkg/tsdb/interval.go:14:21: should omit type time.Duration from declaration of var defaultMinInterval; it will be inferred from the right-hand side
pkg/tsdb/interval.go:15:21: should omit type time.Duration from declaration of var year; it will be inferred from the right-hand side
pkg/tsdb/interval.go:16:21: should omit type time.Duration from declaration of var day; it will be inferred from the right-hand side
pkg/tsdb/cloudwatch/credentials.go:26:24: should omit type map[string]cache from declaration of var awsCredentialCache; it will be inferred from the right-hand side
pkg/tsdb/influxdb/query.go:15:27: should omit type *regexp.Regexp from declaration of var regexpOperatorPattern; it will be inferred from the right-hand side
pkg/tsdb/influxdb/query.go:16:27: should omit type *regexp.Regexp from declaration of var regexpMeasurementPattern; it will be inferred from the right-hand side
pkg/tsdb/mssql/mssql_test.go:25:14: should omit type string from declaration of var serverIP; it will be inferred from the right-hand side
2018-04-27 22:14:36 +02:00

342 lines
8.8 KiB
Go

package dashboards
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
)
var (
checkDiskForChangesInterval = time.Second * 3
ErrFolderNameMissing = errors.New("Folder name missing")
)
type fileReader struct {
Cfg *DashboardsAsConfig
Path string
log log.Logger
dashboardService dashboards.DashboardProvisioningService
}
func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
var path string
path, ok := cfg.Options["path"].(string)
if !ok {
path, ok = cfg.Options["folder"].(string)
if !ok {
return nil, fmt.Errorf("Failed to load dashboards. path param is not a string")
}
log.Warn("[Deprecated] The folder property is deprecated. Please use path instead.")
}
if _, err := os.Stat(path); os.IsNotExist(err) {
log.Error("Cannot read directory", "error", err)
}
return &fileReader{
Cfg: cfg,
Path: path,
log: log,
dashboardService: dashboards.NewProvisioningService(),
}, nil
}
func (fr *fileReader) ReadAndListen(ctx context.Context) error {
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. in-case fs is very slow.
running = true
go func() {
if err := fr.startWalkingDisk(); err != nil {
fr.log.Error("failed to search for dashboards", "error", err)
}
running = false
}()
}
case <-ctx.Done():
return nil
}
}
}
func (fr *fileReader) startWalkingDisk() error {
if _, err := os.Stat(fr.Path); err != nil {
if os.IsNotExist(err) {
return err
}
}
folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardService)
if err != nil && err != ErrFolderNameMissing {
return err
}
provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardService, 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
}
fr.deleteDashboardIfFileIsMissing(provisionedDashboardRefs, filesFoundOnDisk)
sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name)
// save dashboards based on json files
for path, fileInfo := range filesFoundOnDisk {
provisioningMetadata, err := fr.saveDashboard(path, folderId, fileInfo, provisionedDashboardRefs)
sanityChecker.track(provisioningMetadata)
if err != nil {
fr.log.Error("failed to save dashboard", "error", err)
}
}
sanityChecker.logWarnings(fr.log)
return nil
}
func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
if fr.Cfg.DisableDeletion {
return
}
// 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)
}
}
}
func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) (provisioningMetadata, error) {
provisioningMetadata := provisioningMetadata{}
resolvedFileInfo, err := resolveSymlink(fileInfo, path)
if err != nil {
return provisioningMetadata, err
}
provisionedData, alreadyProvisioned := provisionedDashboardRefs[path]
upToDate := alreadyProvisioned && provisionedData.Updated == resolvedFileInfo.ModTime().Unix()
dash, 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
}
// keeps track of what uid's and title's we have already provisioned
provisioningMetadata.uid = dash.Dashboard.Uid
provisioningMetadata.title = dash.Dashboard.Title
if upToDate {
return provisioningMetadata, nil
}
if dash.Dashboard.Id != 0 {
dash.Dashboard.Data.Set("id", nil)
dash.Dashboard.Id = 0
}
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().Unix()}
_, err = fr.dashboardService.SaveProvisionedDashboard(dash, dp)
return provisioningMetadata, err
}
func getProvisionedDashboardByPath(service dashboards.DashboardProvisioningService, name string) (map[string]*models.DashboardProvisioning, error) {
arr, err := service.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, service dashboards.DashboardProvisioningService) (int64, error) {
if cfg.Folder == "" {
return 0, ErrFolderNameMissing
}
cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(cfg.Folder), OrgId: cfg.OrgId}
err := bus.Dispatch(cmd)
if err != nil && err != models.ErrDashboardNotFound {
return 0, err
}
// dashboard folder not found. create one.
if err == models.ErrDashboardNotFound {
dash := &dashboards.SaveDashboardDTO{}
dash.Dashboard = models.NewDashboardFolder(cfg.Folder)
dash.Dashboard.IsFolder = true
dash.Overwrite = true
dash.OrgId = cfg.OrgId
dbDash, err := service.SaveFolderForProvisionedDashboards(dash)
if err != nil {
return 0, err
}
return dbDash.Id, nil
}
if !cmd.Result.IsFolder {
return 0, fmt.Errorf("got invalid response. expected folder, found dashboard")
}
return cmd.Result.Id, nil
}
func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) {
checkFilepath, err := filepath.EvalSymlinks(path)
if path != checkFilepath {
fi, err := os.Lstat(checkFilepath)
if err != nil {
return nil, err
}
return fi, nil
}
return fileinfo, err
}
func createWalkFn(filesOnDisk map[string]os.FileInfo) filepath.WalkFunc {
return func(path string, fileInfo os.FileInfo, err error) error {
if err != nil {
return err
}
isValid, err := validateWalkablePath(fileInfo)
if !isValid {
return err
}
filesOnDisk[path] = fileInfo
return nil
}
}
func validateWalkablePath(fileInfo os.FileInfo) (bool, error) {
if fileInfo.IsDir() {
if strings.HasPrefix(fileInfo.Name(), ".") {
return false, filepath.SkipDir
}
return false, nil
}
if !strings.HasSuffix(fileInfo.Name(), ".json") {
return false, nil
}
return true, nil
}
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
}
defer reader.Close()
data, err := simplejson.NewFromReader(reader)
if err != nil {
return nil, err
}
dash, err := createDashboardJson(data, lastModified, fr.Cfg, folderId)
if err != nil {
return nil, err
}
return dash, nil
}
type provisioningMetadata struct {
uid string
title string
}
func newProvisioningSanityChecker(provisioningProvider string) provisioningSanityChecker {
return provisioningSanityChecker{
provisioningProvider: provisioningProvider,
uidUsage: map[string]uint8{},
titleUsage: map[string]uint8{}}
}
type provisioningSanityChecker struct {
provisioningProvider string
uidUsage map[string]uint8
titleUsage map[string]uint8
}
func (checker provisioningSanityChecker) track(pm provisioningMetadata) {
if len(pm.uid) > 0 {
checker.uidUsage[pm.uid] += 1
}
if len(pm.title) > 0 {
checker.titleUsage[pm.title] += 1
}
}
func (checker provisioningSanityChecker) logWarnings(log log.Logger) {
for uid, times := range checker.uidUsage {
if times > 1 {
log.Error("the same 'uid' is used more than once", "uid", uid, "provider", checker.provisioningProvider)
}
}
for title, times := range checker.titleUsage {
if times > 1 {
log.Error("the same 'title' is used more than once", "title", title, "provider", checker.provisioningProvider)
}
}
}