From ba0285a3ecea090445605cd9bc6cd97ec5826c0b Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 13 Feb 2018 15:47:02 +0100 Subject: [PATCH] provisioning: Warns the user when uid or title is re-used. (#10892) * provisioning: Warns the user when uid or title is re-used. Closes #10880 --- docs/sources/administration/provisioning.md | 9 ++- .../provisioning/dashboards/file_reader.go | 72 ++++++++++++++++--- 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index c3595969281..b70895cd7bc 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -155,7 +155,7 @@ Since not all datasources have the same configuration settings we only have the #### Secure Json data -{"authType":"keys","defaultRegion":"us-west-2","timeField":"@timestamp"} +`{"authType":"keys","defaultRegion":"us-west-2","timeField":"@timestamp"}` Secure json data is a map of settings that will be encrypted with [secret key](/installation/configuration/#secret-key) from the Grafana config. The purpose of this is only to hide content from the users of the application. This should be used for storing TLS Cert and password that Grafana will append to the request on the server side. All of these settings are optional. @@ -169,7 +169,7 @@ Secure json data is a map of settings that will be encrypted with [secret key](/ ### Dashboards -It's possible to manage dashboards in Grafana by adding one or more yaml config files in the [`provisioning/dashboards`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `dashboards providers` that will load dashboards into Grafana. Currently we only support reading dashboards from file but we will add more providers in the future. +It's possible to manage dashboards in Grafana by adding one or more yaml config files in the [`provisioning/dashboards`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `dashboards providers` that will load dashboards into Grafana from the local filesystem. The dashboard provider config file looks somewhat like this: @@ -183,3 +183,8 @@ The dashboard provider config file looks somewhat like this: ``` When Grafana starts, it will update/insert all dashboards available in the configured folders. If you modify the file, the dashboard will also be updated. + +> **Note.** Provisioning allows you to overwrite existing dashboards +> which leads to problems if you re-use settings that are supposed to be unique. +> Be careful not to re-use the same `title` multiple times within a folder +> or `uid` within the same installation as this will cause weird behaviours. diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index c67f355a36e..c909878999e 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -124,37 +124,48 @@ func (fr *fileReader) startWalkingDisk() error { } } + sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name) + // save dashboards based on json files for path, fileInfo := range filesFoundOnDisk { - err = fr.saveDashboard(path, folderId, fileInfo, provisionedDashboardRefs) + 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) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) error { +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 err + return provisioningMetadata, err } provisionedData, alreadyProvisioned := provisionedDashboardRefs[path] - if alreadyProvisioned && provisionedData.Updated.Unix() == resolvedFileInfo.ModTime().Unix() { - return nil // dashboard is already in sync with the database - } + upToDate := alreadyProvisioned && provisionedData.Updated.Unix() == 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 nil + 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 { fr.log.Error("provisioned dashboard json files cannot contain id") - return nil + return provisioningMetadata, nil } if alreadyProvisioned { @@ -164,7 +175,7 @@ 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()} _, err = fr.dashboardRepo.SaveProvisionedDashboard(dash, dp) - return err + return provisioningMetadata, err } func getProvisionedDashboardByPath(repo dashboards.Repository, name string) (map[string]*models.DashboardProvisioning, error) { @@ -280,3 +291,46 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, 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) + } + } + +}