mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Provisioning: Improve validation by validating across all dashboard providers (#26742)
* Provisioning: check sanity across all dashboard readers Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com> * Apply suggestions from code review Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Refactor of duplicateValidator and fix issues according to commentaries Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com> * Apply suggestions from code review Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Remove newDuplicateEntries function Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com> * Change folderUid in logs to folderUID Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com> * Restrict write access for readers, which are provisioning duplicate dashboards Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com> * Fix file reader after rebasing onto master Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com> * Apply suggestions from code review Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Format file_reader Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com> * Apply suggestions from code review Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Apply suggestions from code review Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix lint problem Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com> * Apply suggestions from code review Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com> Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
@@ -30,6 +30,7 @@ type Provisioner struct {
|
|||||||
log log.Logger
|
log log.Logger
|
||||||
fileReaders []*FileReader
|
fileReaders []*FileReader
|
||||||
configs []*config
|
configs []*config
|
||||||
|
duplicateValidator duplicateValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new DashboardProvisioner
|
// New returns a new DashboardProvisioner
|
||||||
@@ -50,6 +51,7 @@ func New(configDirectory string, store dashboards.Store) (DashboardProvisioner,
|
|||||||
log: logger,
|
log: logger,
|
||||||
fileReaders: fileReaders,
|
fileReaders: fileReaders,
|
||||||
configs: configs,
|
configs: configs,
|
||||||
|
duplicateValidator: newDuplicateValidator(logger, fileReaders),
|
||||||
}
|
}
|
||||||
|
|
||||||
return d, nil
|
return d, nil
|
||||||
@@ -70,6 +72,7 @@ func (provider *Provisioner) Provision() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
provider.duplicateValidator.validate()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +95,8 @@ func (provider *Provisioner) PollChanges(ctx context.Context) {
|
|||||||
for _, reader := range provider.fileReaders {
|
for _, reader := range provider.fileReaders {
|
||||||
go reader.pollChanges(ctx)
|
go reader.pollChanges(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go provider.duplicateValidator.Run(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProvisionerResolvedPath returns resolved path for the specified provisioner name. Can be used to generate
|
// GetProvisionerResolvedPath returns resolved path for the specified provisioner name. Can be used to generate
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
@@ -33,6 +34,10 @@ type FileReader struct {
|
|||||||
log log.Logger
|
log log.Logger
|
||||||
dashboardProvisioningService dashboards.DashboardProvisioningService
|
dashboardProvisioningService dashboards.DashboardProvisioningService
|
||||||
FoldersFromFilesStructure bool
|
FoldersFromFilesStructure bool
|
||||||
|
|
||||||
|
mux sync.RWMutex
|
||||||
|
usageTracker *usageTracker
|
||||||
|
dbWriteAccessRestricted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDashboardFileReader returns a new filereader based on `config`
|
// NewDashboardFileReader returns a new filereader based on `config`
|
||||||
@@ -59,6 +64,7 @@ func NewDashboardFileReader(cfg *config, log log.Logger, store dboards.Store) (*
|
|||||||
log: log,
|
log: log,
|
||||||
dashboardProvisioningService: dashboards.NewProvisioningService(store),
|
dashboardProvisioningService: dashboards.NewProvisioningService(store),
|
||||||
FoldersFromFilesStructure: foldersFromFilesStructure,
|
FoldersFromFilesStructure: foldersFromFilesStructure,
|
||||||
|
usageTracker: newUsageTracker(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,25 +105,40 @@ func (fr *FileReader) walkDisk() error {
|
|||||||
|
|
||||||
fr.handleMissingDashboardFiles(provisionedDashboardRefs, filesFoundOnDisk)
|
fr.handleMissingDashboardFiles(provisionedDashboardRefs, filesFoundOnDisk)
|
||||||
|
|
||||||
sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name)
|
usageTracker := newUsageTracker()
|
||||||
|
|
||||||
if fr.FoldersFromFilesStructure {
|
if fr.FoldersFromFilesStructure {
|
||||||
err = fr.storeDashboardsInFoldersFromFileStructure(filesFoundOnDisk, provisionedDashboardRefs, resolvedPath, &sanityChecker)
|
err = fr.storeDashboardsInFoldersFromFileStructure(filesFoundOnDisk, provisionedDashboardRefs, resolvedPath, usageTracker)
|
||||||
} else {
|
} else {
|
||||||
err = fr.storeDashboardsInFolder(filesFoundOnDisk, provisionedDashboardRefs, &sanityChecker)
|
err = fr.storeDashboardsInFolder(filesFoundOnDisk, provisionedDashboardRefs, usageTracker)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sanityChecker.logWarnings(fr.log)
|
fr.mux.Lock()
|
||||||
|
defer fr.mux.Unlock()
|
||||||
|
|
||||||
|
fr.usageTracker = usageTracker
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fr *FileReader) changeWritePermissions(restrict bool) {
|
||||||
|
fr.mux.Lock()
|
||||||
|
defer fr.mux.Unlock()
|
||||||
|
|
||||||
|
fr.dbWriteAccessRestricted = restrict
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fr *FileReader) isDatabaseAccessRestricted() bool {
|
||||||
|
fr.mux.RLock()
|
||||||
|
defer fr.mux.RUnlock()
|
||||||
|
|
||||||
|
return fr.dbWriteAccessRestricted
|
||||||
|
}
|
||||||
|
|
||||||
// storeDashboardsInFolder saves dashboards from the filesystem on disk to the folder from config
|
// storeDashboardsInFolder saves dashboards from the filesystem on disk to the folder from config
|
||||||
func (fr *FileReader) storeDashboardsInFolder(filesFoundOnDisk map[string]os.FileInfo,
|
func (fr *FileReader) storeDashboardsInFolder(filesFoundOnDisk map[string]os.FileInfo,
|
||||||
dashboardRefs map[string]*models.DashboardProvisioning, sanityChecker *provisioningSanityChecker) error {
|
dashboardRefs map[string]*models.DashboardProvisioning, usageTracker *usageTracker) error {
|
||||||
folderID, err := getOrCreateFolderID(fr.Cfg, fr.dashboardProvisioningService, fr.Cfg.Folder)
|
folderID, err := getOrCreateFolderID(fr.Cfg, fr.dashboardProvisioningService, fr.Cfg.Folder)
|
||||||
if err != nil && !errors.Is(err, ErrFolderNameMissing) {
|
if err != nil && !errors.Is(err, ErrFolderNameMissing) {
|
||||||
return err
|
return err
|
||||||
@@ -131,7 +152,7 @@ func (fr *FileReader) storeDashboardsInFolder(filesFoundOnDisk map[string]os.Fil
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
sanityChecker.track(provisioningMetadata)
|
usageTracker.track(provisioningMetadata)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -139,7 +160,7 @@ func (fr *FileReader) storeDashboardsInFolder(filesFoundOnDisk map[string]os.Fil
|
|||||||
// storeDashboardsInFoldersFromFilesystemStructure saves dashboards from the filesystem on disk to the same folder
|
// storeDashboardsInFoldersFromFilesystemStructure saves dashboards from the filesystem on disk to the same folder
|
||||||
// in Grafana as they are in on the filesystem.
|
// in Grafana as they are in on the filesystem.
|
||||||
func (fr *FileReader) storeDashboardsInFoldersFromFileStructure(filesFoundOnDisk map[string]os.FileInfo,
|
func (fr *FileReader) storeDashboardsInFoldersFromFileStructure(filesFoundOnDisk map[string]os.FileInfo,
|
||||||
dashboardRefs map[string]*models.DashboardProvisioning, resolvedPath string, sanityChecker *provisioningSanityChecker) error {
|
dashboardRefs map[string]*models.DashboardProvisioning, resolvedPath string, usageTracker *usageTracker) error {
|
||||||
for path, fileInfo := range filesFoundOnDisk {
|
for path, fileInfo := range filesFoundOnDisk {
|
||||||
folderName := ""
|
folderName := ""
|
||||||
|
|
||||||
@@ -154,7 +175,7 @@ func (fr *FileReader) storeDashboardsInFoldersFromFileStructure(filesFoundOnDisk
|
|||||||
}
|
}
|
||||||
|
|
||||||
provisioningMetadata, err := fr.saveDashboard(path, folderID, fileInfo, dashboardRefs)
|
provisioningMetadata, err := fr.saveDashboard(path, folderID, fileInfo, dashboardRefs)
|
||||||
sanityChecker.track(provisioningMetadata)
|
usageTracker.track(provisioningMetadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fr.log.Error("failed to save dashboard", "error", err)
|
fr.log.Error("failed to save dashboard", "error", err)
|
||||||
}
|
}
|
||||||
@@ -236,6 +257,7 @@ func (fr *FileReader) saveDashboard(path string, folderID int64, fileInfo os.Fil
|
|||||||
dash.Dashboard.SetId(provisionedData.DashboardId)
|
dash.Dashboard.SetId(provisionedData.DashboardId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !fr.isDatabaseAccessRestricted() {
|
||||||
fr.log.Debug("saving new dashboard", "provisioner", fr.Cfg.Name, "file", path, "folderId", dash.Dashboard.FolderId)
|
fr.log.Debug("saving new dashboard", "provisioner", fr.Cfg.Name, "file", path, "folderId", dash.Dashboard.FolderId)
|
||||||
dp := &models.DashboardProvisioning{
|
dp := &models.DashboardProvisioning{
|
||||||
ExternalId: path,
|
ExternalId: path,
|
||||||
@@ -243,9 +265,15 @@ func (fr *FileReader) saveDashboard(path string, folderID int64, fileInfo os.Fil
|
|||||||
Updated: resolvedFileInfo.ModTime().Unix(),
|
Updated: resolvedFileInfo.ModTime().Unix(),
|
||||||
CheckSum: jsonFile.checkSum,
|
CheckSum: jsonFile.checkSum,
|
||||||
}
|
}
|
||||||
|
if _, err := fr.dashboardProvisioningService.SaveProvisionedDashboard(dash, dp); err != nil {
|
||||||
_, err = fr.dashboardProvisioningService.SaveProvisionedDashboard(dash, dp)
|
|
||||||
return provisioningMetadata, err
|
return provisioningMetadata, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fr.log.Warn("Not saving new dashboard due to restricted database access", "provisioner", fr.Cfg.Name,
|
||||||
|
"file", path, "folderId", dash.Dashboard.FolderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return provisioningMetadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getProvisionedDashboardsByPath(service dashboards.DashboardProvisioningService, name string) (
|
func getProvisionedDashboardsByPath(service dashboards.DashboardProvisioningService, name string) (
|
||||||
@@ -412,6 +440,13 @@ func (fr *FileReader) resolvedPath() string {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fr *FileReader) getUsageTracker() *usageTracker {
|
||||||
|
fr.mux.RLock()
|
||||||
|
defer fr.mux.RUnlock()
|
||||||
|
|
||||||
|
return fr.usageTracker
|
||||||
|
}
|
||||||
|
|
||||||
type provisioningMetadata struct {
|
type provisioningMetadata struct {
|
||||||
uid string
|
uid string
|
||||||
identity dashboardIdentity
|
identity dashboardIdentity
|
||||||
@@ -423,42 +458,26 @@ type dashboardIdentity struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardIdentity) Exists() bool {
|
func (d *dashboardIdentity) Exists() bool {
|
||||||
return len(d.title) > 0 && d.folderID > 0
|
return len(d.title) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProvisioningSanityChecker(provisioningProvider string) provisioningSanityChecker {
|
func newUsageTracker() *usageTracker {
|
||||||
return provisioningSanityChecker{
|
return &usageTracker{
|
||||||
provisioningProvider: provisioningProvider,
|
|
||||||
uidUsage: map[string]uint8{},
|
uidUsage: map[string]uint8{},
|
||||||
titleUsage: map[dashboardIdentity]uint8{},
|
titleUsage: map[dashboardIdentity]uint8{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type provisioningSanityChecker struct {
|
type usageTracker struct {
|
||||||
provisioningProvider string
|
|
||||||
uidUsage map[string]uint8
|
uidUsage map[string]uint8
|
||||||
titleUsage map[dashboardIdentity]uint8
|
titleUsage map[dashboardIdentity]uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
func (checker provisioningSanityChecker) track(pm provisioningMetadata) {
|
func (t *usageTracker) track(pm provisioningMetadata) {
|
||||||
if len(pm.uid) > 0 {
|
if len(pm.uid) > 0 {
|
||||||
checker.uidUsage[pm.uid]++
|
t.uidUsage[pm.uid]++
|
||||||
}
|
}
|
||||||
if pm.identity.Exists() {
|
if pm.identity.Exists() {
|
||||||
checker.titleUsage[pm.identity]++
|
t.titleUsage[pm.identity]++
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 identity, times := range checker.titleUsage {
|
|
||||||
if times > 1 {
|
|
||||||
log.Error("the same 'title' is used more than once", "title", identity.title, "provider", checker.provisioningProvider)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ func TestProvisionedSymlinkedFolder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil)
|
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil)
|
||||||
require.NoError(t, err)
|
if err != nil {
|
||||||
|
t.Error("expected err to be nil")
|
||||||
|
}
|
||||||
|
|
||||||
want, err := filepath.Abs(containingID)
|
want, err := filepath.Abs(containingID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
173
pkg/services/provisioning/dashboards/testdata/test-dashboards/dashboard-with-uid/dashboard1.json
vendored
Normal file
173
pkg/services/provisioning/dashboards/testdata/test-dashboards/dashboard-with-uid/dashboard1.json
vendored
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
{
|
||||||
|
"title": "Grafana",
|
||||||
|
"tags": [],
|
||||||
|
"style": "dark",
|
||||||
|
"timezone": "browser",
|
||||||
|
"editable": true,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"title": "New row",
|
||||||
|
"height": "150px",
|
||||||
|
"collapse": false,
|
||||||
|
"editable": true,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"span": 12,
|
||||||
|
"editable": true,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||||
|
"style": {},
|
||||||
|
"title": "Welcome to"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Welcome to Grafana",
|
||||||
|
"height": "210px",
|
||||||
|
"collapse": false,
|
||||||
|
"editable": true,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"span": 6,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||||
|
"style": {},
|
||||||
|
"title": "Documentation Links"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"span": 6,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||||
|
"style": {},
|
||||||
|
"title": "Tips & Shortcuts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "test",
|
||||||
|
"height": "250px",
|
||||||
|
"editable": true,
|
||||||
|
"collapse": false,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"span": 12,
|
||||||
|
"type": "graph",
|
||||||
|
"x-axis": true,
|
||||||
|
"y-axis": true,
|
||||||
|
"scale": 1,
|
||||||
|
"y_formats": [
|
||||||
|
"short",
|
||||||
|
"short"
|
||||||
|
],
|
||||||
|
"grid": {
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"leftMax": null,
|
||||||
|
"rightMax": null,
|
||||||
|
"leftMin": null,
|
||||||
|
"rightMin": null,
|
||||||
|
"threshold1": null,
|
||||||
|
"threshold2": null,
|
||||||
|
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||||
|
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||||
|
},
|
||||||
|
"resolution": 100,
|
||||||
|
"lines": true,
|
||||||
|
"fill": 1,
|
||||||
|
"linewidth": 2,
|
||||||
|
"dashes": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"spaceLength": 10,
|
||||||
|
"points": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"bars": false,
|
||||||
|
"stack": true,
|
||||||
|
"spyable": true,
|
||||||
|
"options": false,
|
||||||
|
"legend": {
|
||||||
|
"show": true,
|
||||||
|
"values": false,
|
||||||
|
"min": false,
|
||||||
|
"max": false,
|
||||||
|
"current": false,
|
||||||
|
"total": false,
|
||||||
|
"avg": false
|
||||||
|
},
|
||||||
|
"interactive": true,
|
||||||
|
"legend_counts": true,
|
||||||
|
"timezone": "browser",
|
||||||
|
"percentage": false,
|
||||||
|
"nullPointMode": "connected",
|
||||||
|
"steppedLine": false,
|
||||||
|
"tooltip": {
|
||||||
|
"value_type": "cumulative",
|
||||||
|
"query_as_alias": true
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"target": "randomWalk('random walk')",
|
||||||
|
"function": "mean",
|
||||||
|
"column": "value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aliasColors": {},
|
||||||
|
"aliasYAxis": {},
|
||||||
|
"title": "First Graph (click title to edit)",
|
||||||
|
"datasource": "graphite",
|
||||||
|
"renderer": "flot",
|
||||||
|
"annotate": {
|
||||||
|
"enable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nav": [
|
||||||
|
{
|
||||||
|
"type": "timepicker",
|
||||||
|
"collapse": false,
|
||||||
|
"enable": true,
|
||||||
|
"status": "Stable",
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
],
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"now": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"version": 5,
|
||||||
|
"uid": "Z-phNqGmz"
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
{
|
||||||
|
"title": "Grafana",
|
||||||
|
"tags": [],
|
||||||
|
"style": "dark",
|
||||||
|
"timezone": "browser",
|
||||||
|
"editable": true,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"title": "New row",
|
||||||
|
"height": "150px",
|
||||||
|
"collapse": false,
|
||||||
|
"editable": true,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"span": 12,
|
||||||
|
"editable": true,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||||
|
"style": {},
|
||||||
|
"title": "Welcome to"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Welcome to Grafana",
|
||||||
|
"height": "210px",
|
||||||
|
"collapse": false,
|
||||||
|
"editable": true,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"span": 6,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||||
|
"style": {},
|
||||||
|
"title": "Documentation Links"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"span": 6,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||||
|
"style": {},
|
||||||
|
"title": "Tips & Shortcuts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "test",
|
||||||
|
"height": "250px",
|
||||||
|
"editable": true,
|
||||||
|
"collapse": false,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"span": 12,
|
||||||
|
"type": "graph",
|
||||||
|
"x-axis": true,
|
||||||
|
"y-axis": true,
|
||||||
|
"scale": 1,
|
||||||
|
"y_formats": [
|
||||||
|
"short",
|
||||||
|
"short"
|
||||||
|
],
|
||||||
|
"grid": {
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"leftMax": null,
|
||||||
|
"rightMax": null,
|
||||||
|
"leftMin": null,
|
||||||
|
"rightMin": null,
|
||||||
|
"threshold1": null,
|
||||||
|
"threshold2": null,
|
||||||
|
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||||
|
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||||
|
},
|
||||||
|
"resolution": 100,
|
||||||
|
"lines": true,
|
||||||
|
"fill": 1,
|
||||||
|
"linewidth": 2,
|
||||||
|
"dashes": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"spaceLength": 10,
|
||||||
|
"points": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"bars": false,
|
||||||
|
"stack": true,
|
||||||
|
"spyable": true,
|
||||||
|
"options": false,
|
||||||
|
"legend": {
|
||||||
|
"show": true,
|
||||||
|
"values": false,
|
||||||
|
"min": false,
|
||||||
|
"max": false,
|
||||||
|
"current": false,
|
||||||
|
"total": false,
|
||||||
|
"avg": false
|
||||||
|
},
|
||||||
|
"interactive": true,
|
||||||
|
"legend_counts": true,
|
||||||
|
"timezone": "browser",
|
||||||
|
"percentage": false,
|
||||||
|
"nullPointMode": "connected",
|
||||||
|
"steppedLine": false,
|
||||||
|
"tooltip": {
|
||||||
|
"value_type": "cumulative",
|
||||||
|
"query_as_alias": true
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"target": "randomWalk('random walk')",
|
||||||
|
"function": "mean",
|
||||||
|
"column": "value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aliasColors": {},
|
||||||
|
"aliasYAxis": {},
|
||||||
|
"title": "First Graph (click title to edit)",
|
||||||
|
"datasource": "graphite",
|
||||||
|
"renderer": "flot",
|
||||||
|
"annotate": {
|
||||||
|
"enable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nav": [
|
||||||
|
{
|
||||||
|
"type": "timepicker",
|
||||||
|
"collapse": false,
|
||||||
|
"enable": true,
|
||||||
|
"status": "Stable",
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
],
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"now": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"version": 5,
|
||||||
|
"uid": "Z-phNqGmz"
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
{
|
||||||
|
"title": "Grafana",
|
||||||
|
"tags": [],
|
||||||
|
"style": "dark",
|
||||||
|
"timezone": "browser",
|
||||||
|
"editable": true,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"title": "New row",
|
||||||
|
"height": "150px",
|
||||||
|
"collapse": false,
|
||||||
|
"editable": true,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"span": 12,
|
||||||
|
"editable": true,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||||
|
"style": {},
|
||||||
|
"title": "Welcome to"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Welcome to Grafana",
|
||||||
|
"height": "210px",
|
||||||
|
"collapse": false,
|
||||||
|
"editable": true,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"span": 6,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||||
|
"style": {},
|
||||||
|
"title": "Documentation Links"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"span": 6,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||||
|
"style": {},
|
||||||
|
"title": "Tips & Shortcuts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "test",
|
||||||
|
"height": "250px",
|
||||||
|
"editable": true,
|
||||||
|
"collapse": false,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"span": 12,
|
||||||
|
"type": "graph",
|
||||||
|
"x-axis": true,
|
||||||
|
"y-axis": true,
|
||||||
|
"scale": 1,
|
||||||
|
"y_formats": [
|
||||||
|
"short",
|
||||||
|
"short"
|
||||||
|
],
|
||||||
|
"grid": {
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"leftMax": null,
|
||||||
|
"rightMax": null,
|
||||||
|
"leftMin": null,
|
||||||
|
"rightMin": null,
|
||||||
|
"threshold1": null,
|
||||||
|
"threshold2": null,
|
||||||
|
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||||
|
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||||
|
},
|
||||||
|
"resolution": 100,
|
||||||
|
"lines": true,
|
||||||
|
"fill": 1,
|
||||||
|
"linewidth": 2,
|
||||||
|
"dashes": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"spaceLength": 10,
|
||||||
|
"points": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"bars": false,
|
||||||
|
"stack": true,
|
||||||
|
"spyable": true,
|
||||||
|
"options": false,
|
||||||
|
"legend": {
|
||||||
|
"show": true,
|
||||||
|
"values": false,
|
||||||
|
"min": false,
|
||||||
|
"max": false,
|
||||||
|
"current": false,
|
||||||
|
"total": false,
|
||||||
|
"avg": false
|
||||||
|
},
|
||||||
|
"interactive": true,
|
||||||
|
"legend_counts": true,
|
||||||
|
"timezone": "browser",
|
||||||
|
"percentage": false,
|
||||||
|
"nullPointMode": "connected",
|
||||||
|
"steppedLine": false,
|
||||||
|
"tooltip": {
|
||||||
|
"value_type": "cumulative",
|
||||||
|
"query_as_alias": true
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"target": "randomWalk('random walk')",
|
||||||
|
"function": "mean",
|
||||||
|
"column": "value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aliasColors": {},
|
||||||
|
"aliasYAxis": {},
|
||||||
|
"title": "First Graph (click title to edit)",
|
||||||
|
"datasource": "graphite",
|
||||||
|
"renderer": "flot",
|
||||||
|
"annotate": {
|
||||||
|
"enable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nav": [
|
||||||
|
{
|
||||||
|
"type": "timepicker",
|
||||||
|
"collapse": false,
|
||||||
|
"enable": true,
|
||||||
|
"status": "Stable",
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
],
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"now": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"version": 5,
|
||||||
|
"uid": "Z-phNqGmz"
|
||||||
|
}
|
||||||
145
pkg/services/provisioning/dashboards/validator.go
Normal file
145
pkg/services/provisioning/dashboards/validator.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package dashboards
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type duplicate struct {
|
||||||
|
Sum uint8
|
||||||
|
InvolvedReaders map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDuplicate() *duplicate {
|
||||||
|
return &duplicate{InvolvedReaders: make(map[string]struct{})}
|
||||||
|
}
|
||||||
|
|
||||||
|
type duplicateEntries struct {
|
||||||
|
Titles map[dashboardIdentity]*duplicate
|
||||||
|
UIDs map[string]*duplicate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *duplicateEntries) InvolvedReaders() map[string]struct{} {
|
||||||
|
involvedReaders := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, duplicate := range d.UIDs {
|
||||||
|
if duplicate.Sum <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for readerName := range duplicate.InvolvedReaders {
|
||||||
|
involvedReaders[readerName] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, duplicate := range d.Titles {
|
||||||
|
if duplicate.Sum <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for readerName := range duplicate.InvolvedReaders {
|
||||||
|
involvedReaders[readerName] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return involvedReaders
|
||||||
|
}
|
||||||
|
|
||||||
|
type duplicateValidator struct {
|
||||||
|
logger log.Logger
|
||||||
|
readers []*FileReader
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDuplicateValidator(logger log.Logger, readers []*FileReader) duplicateValidator {
|
||||||
|
return duplicateValidator{logger: logger, readers: readers}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *duplicateValidator) getDuplicates() *duplicateEntries {
|
||||||
|
duplicates := duplicateEntries{
|
||||||
|
Titles: make(map[dashboardIdentity]*duplicate),
|
||||||
|
UIDs: make(map[string]*duplicate),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, reader := range c.readers {
|
||||||
|
readerName := reader.Cfg.Name
|
||||||
|
tracker := reader.getUsageTracker()
|
||||||
|
|
||||||
|
for uid, times := range tracker.uidUsage {
|
||||||
|
if _, ok := duplicates.UIDs[uid]; !ok {
|
||||||
|
duplicates.UIDs[uid] = newDuplicate()
|
||||||
|
}
|
||||||
|
duplicates.UIDs[uid].Sum += times
|
||||||
|
duplicates.UIDs[uid].InvolvedReaders[readerName] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, times := range tracker.titleUsage {
|
||||||
|
if _, ok := duplicates.Titles[id]; !ok {
|
||||||
|
duplicates.Titles[id] = newDuplicate()
|
||||||
|
}
|
||||||
|
duplicates.Titles[id].Sum += times
|
||||||
|
duplicates.Titles[id].InvolvedReaders[readerName] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &duplicates
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *duplicateValidator) logWarnings(duplicates *duplicateEntries) {
|
||||||
|
for uid, usage := range duplicates.UIDs {
|
||||||
|
if usage.Sum > 1 {
|
||||||
|
c.logger.Warn("the same UID is used more than once", "uid", uid, "times", usage.Sum, "providers",
|
||||||
|
keysToSlice(usage.InvolvedReaders))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, usage := range duplicates.Titles {
|
||||||
|
if usage.Sum > 1 {
|
||||||
|
c.logger.Warn("dashboard title is not unique in folder", "title", id.title, "folderID", id.folderID, "times",
|
||||||
|
usage.Sum, "providers", keysToSlice(usage.InvolvedReaders))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *duplicateValidator) takeAwayWritePermissions(duplicates *duplicateEntries) {
|
||||||
|
involvedReaders := duplicates.InvolvedReaders()
|
||||||
|
for _, reader := range c.readers {
|
||||||
|
_, isReaderWithDuplicates := involvedReaders[reader.Cfg.Name]
|
||||||
|
// We restrict reader permissions to write to the database here to prevent overloading
|
||||||
|
reader.changeWritePermissions(isReaderWithDuplicates)
|
||||||
|
|
||||||
|
if isReaderWithDuplicates {
|
||||||
|
c.logger.Warn("dashboards provisioning provider has no database write permissions because of duplicates", "provider", reader.Cfg.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *duplicateValidator) Run(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
c.validate()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *duplicateValidator) validate() {
|
||||||
|
duplicates := c.getDuplicates()
|
||||||
|
|
||||||
|
c.logWarnings(duplicates)
|
||||||
|
c.takeAwayWritePermissions(duplicates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func keysToSlice(data map[string]struct{}) []string {
|
||||||
|
entries := make([]string, 0, len(data))
|
||||||
|
|
||||||
|
for entry := range data {
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
124
pkg/services/provisioning/dashboards/validator_test.go
Normal file
124
pkg/services/provisioning/dashboards/validator_test.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package dashboards
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dashboardContainingUID = "testdata/test-dashboards/dashboard-with-uid"
|
||||||
|
twoDashboardsWithUID = "testdata/test-dashboards/two-dashboards-with-uid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDuplicatesValidator(t *testing.T) {
|
||||||
|
bus.ClearBusHandlers()
|
||||||
|
fakeService = mockDashboardProvisioningService()
|
||||||
|
|
||||||
|
bus.AddHandler("test", mockGetDashboardQuery)
|
||||||
|
cfg := &config{
|
||||||
|
Name: "Default",
|
||||||
|
Type: "file",
|
||||||
|
OrgID: 1,
|
||||||
|
Folder: "",
|
||||||
|
Options: map[string]interface{}{},
|
||||||
|
}
|
||||||
|
logger := log.New("test.logger")
|
||||||
|
|
||||||
|
t.Run("Duplicates validator should collect info about duplicate UIDs and titles within folders", func(t *testing.T) {
|
||||||
|
const folderName = "duplicates-validator-folder"
|
||||||
|
folderID, err := getOrCreateFolderID(cfg, fakeService, folderName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
identity := dashboardIdentity{folderID: folderID, title: "Grafana"}
|
||||||
|
|
||||||
|
cfg1 := &config{
|
||||||
|
Name: "first", Type: "file", OrgID: 1, Folder: folderName,
|
||||||
|
Options: map[string]interface{}{"path": dashboardContainingUID},
|
||||||
|
}
|
||||||
|
cfg2 := &config{
|
||||||
|
Name: "second", Type: "file", OrgID: 1, Folder: folderName,
|
||||||
|
Options: map[string]interface{}{"path": dashboardContainingUID},
|
||||||
|
}
|
||||||
|
|
||||||
|
reader1, err := NewDashboardFileReader(cfg1, logger, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
reader2, err := NewDashboardFileReader(cfg2, logger, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
duplicateValidator := newDuplicateValidator(logger, []*FileReader{reader1, reader2})
|
||||||
|
|
||||||
|
err = reader1.walkDisk()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = reader2.walkDisk()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
duplicates := duplicateValidator.getDuplicates()
|
||||||
|
|
||||||
|
require.Equal(t, uint8(2), duplicates.UIDs["Z-phNqGmz"].Sum)
|
||||||
|
uidUsageReaders := keysToSlice(duplicates.UIDs["Z-phNqGmz"].InvolvedReaders)
|
||||||
|
sort.Strings(uidUsageReaders)
|
||||||
|
require.Equal(t, []string{"first", "second"}, uidUsageReaders)
|
||||||
|
|
||||||
|
require.Equal(t, uint8(2), duplicates.Titles[identity].Sum)
|
||||||
|
titleUsageReaders := keysToSlice(duplicates.Titles[identity].InvolvedReaders)
|
||||||
|
sort.Strings(titleUsageReaders)
|
||||||
|
require.Equal(t, []string{"first", "second"}, titleUsageReaders)
|
||||||
|
|
||||||
|
duplicateValidator.validate()
|
||||||
|
require.True(t, reader1.isDatabaseAccessRestricted())
|
||||||
|
require.True(t, reader2.isDatabaseAccessRestricted())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Duplicates validator should restrict write access only for readers with duplicates", func(t *testing.T) {
|
||||||
|
cfg1 := &config{
|
||||||
|
Name: "first", Type: "file", OrgID: 1, Folder: "duplicates-validator-folder",
|
||||||
|
Options: map[string]interface{}{"path": twoDashboardsWithUID},
|
||||||
|
}
|
||||||
|
cfg2 := &config{
|
||||||
|
Name: "second", Type: "file", OrgID: 1, Folder: "root",
|
||||||
|
Options: map[string]interface{}{"path": defaultDashboards},
|
||||||
|
}
|
||||||
|
|
||||||
|
reader1, err := NewDashboardFileReader(cfg1, logger, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
reader2, err := NewDashboardFileReader(cfg2, logger, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
duplicateValidator := newDuplicateValidator(logger, []*FileReader{reader1, reader2})
|
||||||
|
|
||||||
|
err = reader1.walkDisk()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = reader2.walkDisk()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
duplicates := duplicateValidator.getDuplicates()
|
||||||
|
|
||||||
|
folderID, err := getOrCreateFolderID(cfg, fakeService, cfg1.Folder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
identity := dashboardIdentity{folderID: folderID, title: "Grafana"}
|
||||||
|
|
||||||
|
require.Equal(t, uint8(2), duplicates.UIDs["Z-phNqGmz"].Sum)
|
||||||
|
uidUsageReaders := keysToSlice(duplicates.UIDs["Z-phNqGmz"].InvolvedReaders)
|
||||||
|
sort.Strings(uidUsageReaders)
|
||||||
|
require.Equal(t, []string{"first"}, uidUsageReaders)
|
||||||
|
|
||||||
|
require.Equal(t, uint8(2), duplicates.Titles[identity].Sum)
|
||||||
|
titleUsageReaders := keysToSlice(duplicates.Titles[identity].InvolvedReaders)
|
||||||
|
sort.Strings(titleUsageReaders)
|
||||||
|
require.Equal(t, []string{"first"}, titleUsageReaders)
|
||||||
|
|
||||||
|
duplicateValidator.validate()
|
||||||
|
require.True(t, reader1.isDatabaseAccessRestricted())
|
||||||
|
require.False(t, reader2.isDatabaseAccessRestricted())
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user