Commit fca5ee4b by Josh Committed by Andrej Ocenas

Provisioning: Support FolderUid in Dashboard Provisioning Config (#16559)

* add folderUid to DashbaordsAsConfig structs and DashbardProviderConfigs struct, set these values in mapping func
look for new folderUid values in config_reader tests
set dashboard folder Uid explicitly in file_reader, which has no affect when not given

* formatting and docstrings

* add folderUid to DashbaordsAsConfig structs and DashbardProviderConfigs struct, set these values in mapping func
look for new folderUid values in config_reader tests
set dashboard folder Uid explicitly in file_reader, which has no affect when not given

* formatting and docstrings

* add folderUid option, as well as documentation for the rest of the fields

* add blank folderUid in devenv example.

* add folderUid to provisioning sample yaml

* instead of just warning, return error if unmarshalling dashboard provisioning file fails

* Removing the error handling and adding comment

* Add duplicity check for folder Uids


Co-authored-by: swtch1 <joshua.thornton@protonmail.com>
parent 19e82400
...@@ -5,6 +5,7 @@ apiVersion: 1 ...@@ -5,6 +5,7 @@ apiVersion: 1
# - name: 'default' # - name: 'default'
# orgId: 1 # orgId: 1
# folder: '' # folder: ''
# folderUid: ''
# type: file # type: file
# options: # options:
# path: /var/lib/grafana/dashboards # path: /var/lib/grafana/dashboards
...@@ -3,6 +3,7 @@ apiVersion: 1 ...@@ -3,6 +3,7 @@ apiVersion: 1
providers: providers:
- name: 'gdev dashboards' - name: 'gdev dashboards'
folder: 'gdev dashboards' folder: 'gdev dashboards'
folderUid: ''
type: file type: file
updateIntervalSeconds: 60 updateIntervalSeconds: 60
options: options:
......
...@@ -203,13 +203,24 @@ The dashboard provider config file looks somewhat like this: ...@@ -203,13 +203,24 @@ The dashboard provider config file looks somewhat like this:
apiVersion: 1 apiVersion: 1
providers: providers:
# <string> provider name
- name: 'default' - name: 'default'
# <int> org id. will default to orgId 1 if not specified
orgId: 1 orgId: 1
# <string, required> name of the dashboard folder. Required
folder: '' folder: ''
# <string> folder UID. will be automatically generated if not specified
folderUid: ''
# <string, required> provider type. Required
type: file type: file
# <bool> disable dashboard deletion
disableDeletion: false disableDeletion: false
updateIntervalSeconds: 10 #how often Grafana will scan for changed dashboards # <bool> enable dashboard editing
editable: true
# <int> how often Grafana will scan for changed dashboards
updateIntervalSeconds: 10
options: options:
# <string, required> path to dashboard files on disk. Required
path: /var/lib/grafana/dashboards path: /var/lib/grafana/dashboards
``` ```
......
...@@ -24,10 +24,15 @@ func (cr *configReader) parseConfigs(file os.FileInfo) ([]*DashboardsAsConfig, e ...@@ -24,10 +24,15 @@ func (cr *configReader) parseConfigs(file os.FileInfo) ([]*DashboardsAsConfig, e
} }
apiVersion := &ConfigVersion{ApiVersion: 0} apiVersion := &ConfigVersion{ApiVersion: 0}
yaml.Unmarshal(yamlFile, &apiVersion)
if apiVersion.ApiVersion > 0 { // We ignore the error here because it errors out for version 0 which does not have apiVersion
// specified (so 0 is default). This can also error in case the apiVersion is not an integer but at the moment
// this does not handle that case and would still go on as if version = 0.
// TODO: return appropriate error in case the apiVersion is specified but isn't integer (or even if it is
// integer > max version?).
_ = yaml.Unmarshal(yamlFile, &apiVersion)
if apiVersion.ApiVersion > 0 {
v1 := &DashboardAsConfigV1{} v1 := &DashboardAsConfigV1{}
err := yaml.Unmarshal(yamlFile, &v1) err := yaml.Unmarshal(yamlFile, &v1)
if err != nil { if err != nil {
...@@ -37,7 +42,6 @@ func (cr *configReader) parseConfigs(file os.FileInfo) ([]*DashboardsAsConfig, e ...@@ -37,7 +42,6 @@ func (cr *configReader) parseConfigs(file os.FileInfo) ([]*DashboardsAsConfig, e
if v1 != nil { if v1 != nil {
return v1.mapToDashboardAsConfig(), nil return v1.mapToDashboardAsConfig(), nil
} }
} else { } else {
var v0 []*DashboardsAsConfigV0 var v0 []*DashboardsAsConfigV0
err := yaml.Unmarshal(yamlFile, &v0) err := yaml.Unmarshal(yamlFile, &v0)
...@@ -78,13 +82,23 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { ...@@ -78,13 +82,23 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
} }
} }
for i := range dashboards { uidUsage := map[string]uint8{}
if dashboards[i].OrgId == 0 { for _, dashboard := range dashboards {
dashboards[i].OrgId = 1 if dashboard.OrgId == 0 {
dashboard.OrgId = 1
} }
if dashboards[i].UpdateIntervalSeconds == 0 { if dashboard.UpdateIntervalSeconds == 0 {
dashboards[i].UpdateIntervalSeconds = 10 dashboard.UpdateIntervalSeconds = 10
}
if len(dashboard.FolderUid) > 0 {
uidUsage[dashboard.FolderUid] += 1
}
}
for uid, times := range uidUsage {
if times > 1 {
cr.log.Error("the same 'folderUid' is used more than once", "folderUid", uid)
} }
} }
......
...@@ -66,6 +66,7 @@ func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) { ...@@ -66,6 +66,7 @@ func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) {
So(ds.Type, ShouldEqual, "file") So(ds.Type, ShouldEqual, "file")
So(ds.OrgId, ShouldEqual, 2) So(ds.OrgId, ShouldEqual, 2)
So(ds.Folder, ShouldEqual, "developers") So(ds.Folder, ShouldEqual, "developers")
So(ds.FolderUid, ShouldEqual, "xyz")
So(ds.Editable, ShouldBeTrue) So(ds.Editable, ShouldBeTrue)
So(len(ds.Options), ShouldEqual, 1) So(len(ds.Options), ShouldEqual, 1)
So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
...@@ -77,6 +78,7 @@ func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) { ...@@ -77,6 +78,7 @@ func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) {
So(ds2.Type, ShouldEqual, "file") So(ds2.Type, ShouldEqual, "file")
So(ds2.OrgId, ShouldEqual, 1) So(ds2.OrgId, ShouldEqual, 1)
So(ds2.Folder, ShouldEqual, "") So(ds2.Folder, ShouldEqual, "")
So(ds2.FolderUid, ShouldEqual, "")
So(ds2.Editable, ShouldBeFalse) So(ds2.Editable, ShouldBeFalse)
So(len(ds2.Options), ShouldEqual, 1) So(len(ds2.Options), ShouldEqual, 1)
So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
......
...@@ -78,6 +78,7 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error { ...@@ -78,6 +78,7 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error {
} }
} }
// startWalkingDisk finds and saves dashboards on disk.
func (fr *fileReader) startWalkingDisk() error { func (fr *fileReader) startWalkingDisk() error {
resolvedPath := fr.resolvePath(fr.Path) resolvedPath := fr.resolvePath(fr.Path)
if _, err := os.Stat(resolvedPath); err != nil { if _, err := os.Stat(resolvedPath); err != nil {
...@@ -119,6 +120,7 @@ func (fr *fileReader) startWalkingDisk() error { ...@@ -119,6 +120,7 @@ func (fr *fileReader) startWalkingDisk() error {
return nil return nil
} }
// handleMissingDashboardFiles will unprovision or delete dashboards which are missing on disk.
func (fr *fileReader) handleMissingDashboardFiles(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) { func (fr *fileReader) handleMissingDashboardFiles(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
// find dashboards to delete since json file is missing // find dashboards to delete since json file is missing
var dashboardToDelete []int64 var dashboardToDelete []int64
...@@ -151,6 +153,7 @@ func (fr *fileReader) handleMissingDashboardFiles(provisionedDashboardRefs map[s ...@@ -151,6 +153,7 @@ func (fr *fileReader) handleMissingDashboardFiles(provisionedDashboardRefs map[s
} }
} }
// saveDashboard saves or updates the dashboard provisioning file at path.
func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) (provisioningMetadata, error) { func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) (provisioningMetadata, error) {
provisioningMetadata := provisioningMetadata{} provisioningMetadata := provisioningMetadata{}
resolvedFileInfo, err := resolveSymlink(fileInfo, path) resolvedFileInfo, err := resolveSymlink(fileInfo, path)
...@@ -189,7 +192,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil ...@@ -189,7 +192,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
dash.Dashboard.SetId(provisionedData.DashboardId) dash.Dashboard.SetId(provisionedData.DashboardId)
} }
fr.log.Debug("saving new dashboard", "provisoner", 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,
Name: fr.Cfg.Name, Name: fr.Cfg.Name,
...@@ -234,6 +237,8 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, service dashboards.DashboardPr ...@@ -234,6 +237,8 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, service dashboards.DashboardPr
dash.Dashboard.IsFolder = true dash.Dashboard.IsFolder = true
dash.Overwrite = true dash.Overwrite = true
dash.OrgId = cfg.OrgId dash.OrgId = cfg.OrgId
// set dashboard folderUid if given
dash.Dashboard.SetUid(cfg.FolderUid)
dbDash, err := service.SaveFolderForProvisionedDashboards(dash) dbDash, err := service.SaveFolderForProvisionedDashboards(dash)
if err != nil { if err != nil {
return 0, err return 0, err
......
...@@ -4,6 +4,7 @@ providers: ...@@ -4,6 +4,7 @@ providers:
- name: 'general dashboards' - name: 'general dashboards'
orgId: 2 orgId: 2
folder: 'developers' folder: 'developers'
folderUid: 'xyz'
editable: true editable: true
disableDeletion: true disableDeletion: true
updateIntervalSeconds: 15 updateIntervalSeconds: 15
......
- name: 'general dashboards' - name: 'general dashboards'
org_id: 2 org_id: 2
folder: 'developers' folder: 'developers'
folderUid: 'xyz'
editable: true editable: true
disableDeletion: true disableDeletion: true
updateIntervalSeconds: 15 updateIntervalSeconds: 15
......
...@@ -14,6 +14,7 @@ type DashboardsAsConfig struct { ...@@ -14,6 +14,7 @@ type DashboardsAsConfig struct {
Type string Type string
OrgId int64 OrgId int64
Folder string Folder string
FolderUid string
Editable bool Editable bool
Options map[string]interface{} Options map[string]interface{}
DisableDeletion bool DisableDeletion bool
...@@ -25,6 +26,7 @@ type DashboardsAsConfigV0 struct { ...@@ -25,6 +26,7 @@ type DashboardsAsConfigV0 struct {
Type string `json:"type" yaml:"type"` Type string `json:"type" yaml:"type"`
OrgId int64 `json:"org_id" yaml:"org_id"` OrgId int64 `json:"org_id" yaml:"org_id"`
Folder string `json:"folder" yaml:"folder"` Folder string `json:"folder" yaml:"folder"`
FolderUid string `json:"folderUid" yaml:"folderUid"`
Editable bool `json:"editable" yaml:"editable"` Editable bool `json:"editable" yaml:"editable"`
Options map[string]interface{} `json:"options" yaml:"options"` Options map[string]interface{} `json:"options" yaml:"options"`
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
...@@ -44,6 +46,7 @@ type DashboardProviderConfigs struct { ...@@ -44,6 +46,7 @@ type DashboardProviderConfigs struct {
Type string `json:"type" yaml:"type"` Type string `json:"type" yaml:"type"`
OrgId int64 `json:"orgId" yaml:"orgId"` OrgId int64 `json:"orgId" yaml:"orgId"`
Folder string `json:"folder" yaml:"folder"` Folder string `json:"folder" yaml:"folder"`
FolderUid string `json:"folderUid" yaml:"folderUid"`
Editable bool `json:"editable" yaml:"editable"` Editable bool `json:"editable" yaml:"editable"`
Options map[string]interface{} `json:"options" yaml:"options"` Options map[string]interface{} `json:"options" yaml:"options"`
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
...@@ -75,6 +78,7 @@ func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig ...@@ -75,6 +78,7 @@ func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig
Type: v.Type, Type: v.Type,
OrgId: v.OrgId, OrgId: v.OrgId,
Folder: v.Folder, Folder: v.Folder,
FolderUid: v.FolderUid,
Editable: v.Editable, Editable: v.Editable,
Options: v.Options, Options: v.Options,
DisableDeletion: v.DisableDeletion, DisableDeletion: v.DisableDeletion,
...@@ -94,6 +98,7 @@ func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig { ...@@ -94,6 +98,7 @@ func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig {
Type: v.Type, Type: v.Type,
OrgId: v.OrgId, OrgId: v.OrgId,
Folder: v.Folder, Folder: v.Folder,
FolderUid: v.FolderUid,
Editable: v.Editable, Editable: v.Editable,
Options: v.Options, Options: v.Options,
DisableDeletion: v.DisableDeletion, DisableDeletion: v.DisableDeletion,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment