diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 26d00d18e51..bd32543fc04 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -140,14 +140,14 @@ func (fr *FileReader) isDatabaseAccessRestricted() bool { // storeDashboardsInFolder saves dashboards from the filesystem on disk to the folder from config func (fr *FileReader) storeDashboardsInFolder(ctx context.Context, filesFoundOnDisk map[string]os.FileInfo, dashboardRefs map[string]*dashboards.DashboardProvisioning, usageTracker *usageTracker) error { - folderID, err := fr.getOrCreateFolderID(ctx, fr.Cfg, fr.dashboardProvisioningService, fr.Cfg.Folder) + folderID, folderUID, err := fr.getOrCreateFolder(ctx, fr.Cfg, fr.dashboardProvisioningService, fr.Cfg.Folder) if err != nil && !errors.Is(err, ErrFolderNameMissing) { return err } // save dashboards based on json files for path, fileInfo := range filesFoundOnDisk { - provisioningMetadata, err := fr.saveDashboard(ctx, path, folderID, fileInfo, dashboardRefs) + provisioningMetadata, err := fr.saveDashboard(ctx, path, folderID, folderUID, fileInfo, dashboardRefs) if err != nil { fr.log.Error("failed to save dashboard", "file", path, "error", err) continue @@ -170,12 +170,12 @@ func (fr *FileReader) storeDashboardsInFoldersFromFileStructure(ctx context.Cont folderName = filepath.Base(dashboardsFolder) } - folderID, err := fr.getOrCreateFolderID(ctx, fr.Cfg, fr.dashboardProvisioningService, folderName) + folderID, folderUID, err := fr.getOrCreateFolder(ctx, fr.Cfg, fr.dashboardProvisioningService, folderName) if err != nil && !errors.Is(err, ErrFolderNameMissing) { return fmt.Errorf("can't provision folder %q from file system structure: %w", folderName, err) } - provisioningMetadata, err := fr.saveDashboard(ctx, path, folderID, fileInfo, dashboardRefs) + provisioningMetadata, err := fr.saveDashboard(ctx, path, folderID, folderUID, fileInfo, dashboardRefs) usageTracker.track(provisioningMetadata) if err != nil { fr.log.Error("failed to save dashboard", "file", path, "error", err) @@ -219,7 +219,7 @@ func (fr *FileReader) handleMissingDashboardFiles(ctx context.Context, provision } // saveDashboard saves or updates the dashboard provisioning file at path. -func (fr *FileReader) saveDashboard(ctx context.Context, path string, folderID int64, fileInfo os.FileInfo, +func (fr *FileReader) saveDashboard(ctx context.Context, path string, folderID int64, folderUID string, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*dashboards.DashboardProvisioning) (provisioningMetadata, error) { provisioningMetadata := provisioningMetadata{} resolvedFileInfo, err := resolveSymlink(fileInfo, path) @@ -229,7 +229,7 @@ func (fr *FileReader) saveDashboard(ctx context.Context, path string, folderID i provisionedData, alreadyProvisioned := provisionedDashboardRefs[path] - jsonFile, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderID) + jsonFile, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderID, folderUID) if err != nil { fr.log.Error("failed to load dashboard from ", "file", path, "error", err) return provisioningMetadata, nil @@ -245,7 +245,25 @@ func (fr *FileReader) saveDashboard(ctx context.Context, path string, folderID i provisioningMetadata.uid = dash.Dashboard.UID provisioningMetadata.identity = dashboardIdentity{title: dash.Dashboard.Title, folderID: dash.Dashboard.FolderID} + // fix empty folder_uid from already provisioned dashboards + if upToDate && folderUID != "" { + d, err := fr.dashboardStore.GetDashboard( + ctx, + &dashboards.GetDashboardQuery{ + OrgID: jsonFile.dashboard.OrgID, + UID: jsonFile.dashboard.Dashboard.UID, + }, + ) + if err != nil { + return provisioningMetadata, err + } + if d.FolderUID != folderUID { + upToDate = false + } + } + if upToDate { + fr.log.Debug("provisioned dashboard is up to date", "provisioner", fr.Cfg.Name, "file", path, "folderId", dash.Dashboard.FolderID, "folderUid", dash.Dashboard.FolderUID) return provisioningMetadata, nil } @@ -259,7 +277,7 @@ func (fr *FileReader) saveDashboard(ctx context.Context, path string, folderID i } 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, "folderUid", dash.Dashboard.FolderUID) dp := &dashboards.DashboardProvisioning{ ExternalID: path, Name: fr.Cfg.Name, @@ -293,9 +311,9 @@ func getProvisionedDashboardsByPath(ctx context.Context, service dashboards.Dash return byPath, nil } -func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, service dashboards.DashboardProvisioningService, folderName string) (int64, error) { +func (fr *FileReader) getOrCreateFolder(ctx context.Context, cfg *config, service dashboards.DashboardProvisioningService, folderName string) (int64, string, error) { if folderName == "" { - return 0, ErrFolderNameMissing + return 0, "", ErrFolderNameMissing } cmd := &dashboards.GetDashboardQuery{ @@ -306,7 +324,7 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv result, err := fr.dashboardStore.GetDashboard(ctx, cmd) if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { - return 0, err + return 0, "", err } // dashboard folder not found. create one. @@ -318,22 +336,22 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv dash.OrgID = cfg.OrgID // set dashboard folderUid if given if cfg.FolderUID == accesscontrol.GeneralFolderUID { - return 0, dashboards.ErrFolderInvalidUID + return 0, "", dashboards.ErrFolderInvalidUID } dash.Dashboard.SetUID(cfg.FolderUID) dbDash, err := service.SaveFolderForProvisionedDashboards(ctx, dash) if err != nil { - return 0, err + return 0, "", err } - return dbDash.ID, nil + return dbDash.ID, dbDash.UID, nil } if !result.IsFolder { - return 0, fmt.Errorf("got invalid response. expected folder, found dashboard") + return 0, "", fmt.Errorf("got invalid response. expected folder, found dashboard") } - return result.ID, nil + return result.ID, result.UID, nil } func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) { @@ -387,7 +405,7 @@ type dashboardJSONFile struct { lastModified time.Time } -func (fr *FileReader) readDashboardFromFile(path string, lastModified time.Time, folderID int64) (*dashboardJSONFile, error) { +func (fr *FileReader) readDashboardFromFile(path string, lastModified time.Time, folderID int64, folderUID string) (*dashboardJSONFile, error) { // nolint:gosec // We can ignore the gosec G304 warning on this one because `path` comes from the provisioning configuration file. reader, err := os.Open(path) @@ -415,7 +433,7 @@ func (fr *FileReader) readDashboardFromFile(path string, lastModified time.Time, return nil, err } - dash, err := createDashboardJSON(data, lastModified, fr.Cfg, folderID) + dash, err := createDashboardJSON(data, lastModified, fr.Cfg, folderID, folderUID) if err != nil { return nil, err } diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index d1ce737b4ea..d886b11fe8b 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -364,7 +364,7 @@ func TestDashboardFileReader(t *testing.T) { r, err := NewDashboardFileReader(cfg, logger, nil, nil) require.NoError(t, err) - _, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) + _, _, err = r.getOrCreateFolder(context.Background(), cfg, fakeService, cfg.Folder) require.Equal(t, err, ErrFolderNameMissing) }) @@ -384,7 +384,7 @@ func TestDashboardFileReader(t *testing.T) { r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) - _, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) + _, _, err = r.getOrCreateFolder(context.Background(), cfg, fakeService, cfg.Folder) require.NoError(t, err) }) @@ -404,7 +404,7 @@ func TestDashboardFileReader(t *testing.T) { r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) - _, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) + _, _, err = r.getOrCreateFolder(context.Background(), cfg, fakeService, cfg.Folder) require.ErrorIs(t, err, dashboards.ErrFolderInvalidUID) }) diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index e19808b89f9..dbe11a0aa84 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -56,7 +56,7 @@ type configs struct { AllowUIUpdates values.BoolValue `json:"allowUiUpdates" yaml:"allowUiUpdates"` } -func createDashboardJSON(data *simplejson.Json, lastModified time.Time, cfg *config, folderID int64) (*dashboards.SaveDashboardDTO, error) { +func createDashboardJSON(data *simplejson.Json, lastModified time.Time, cfg *config, folderID int64, folderUID string) (*dashboards.SaveDashboardDTO, error) { dash := &dashboards.SaveDashboardDTO{} dash.Dashboard = dashboards.NewDashboardFromJson(data) dash.UpdatedAt = lastModified @@ -64,6 +64,7 @@ func createDashboardJSON(data *simplejson.Json, lastModified time.Time, cfg *con dash.OrgID = cfg.OrgID dash.Dashboard.OrgID = cfg.OrgID dash.Dashboard.FolderID = folderID + dash.Dashboard.FolderUID = folderUID if dash.Dashboard.Title == "" { return nil, dashboards.ErrDashboardTitleEmpty diff --git a/pkg/services/provisioning/dashboards/validator_test.go b/pkg/services/provisioning/dashboards/validator_test.go index bc2bc494bea..7a2780f5a4b 100644 --- a/pkg/services/provisioning/dashboards/validator_test.go +++ b/pkg/services/provisioning/dashboards/validator_test.go @@ -39,7 +39,7 @@ func TestDuplicatesValidator(t *testing.T) { fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(6) fakeService.On("GetProvisionedDashboardData", mock.Anything, mock.AnythingOfType("string")).Return([]*dashboards.DashboardProvisioning{}, nil).Times(4) fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5) - folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, folderName) + folderID, _, err := r.getOrCreateFolder(context.Background(), cfg, fakeService, folderName) require.NoError(t, err) identity := dashboardIdentity{folderID: folderID, title: "Grafana"} @@ -92,7 +92,7 @@ func TestDuplicatesValidator(t *testing.T) { fakeStore := &fakeDashboardStore{} r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) - folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, folderName) + folderID, _, err := r.getOrCreateFolder(context.Background(), cfg, fakeService, folderName) require.NoError(t, err) identity := dashboardIdentity{folderID: folderID, title: "Grafana"} @@ -194,7 +194,7 @@ func TestDuplicatesValidator(t *testing.T) { r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) - folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg1.Folder) + folderID, _, err := r.getOrCreateFolder(context.Background(), cfg, fakeService, cfg1.Folder) require.NoError(t, err) identity := dashboardIdentity{folderID: folderID, title: "Grafana"} @@ -211,7 +211,7 @@ func TestDuplicatesValidator(t *testing.T) { r, err = NewDashboardFileReader(cfg3, logger, nil, fakeStore) require.NoError(t, err) - folderID, err = r.getOrCreateFolderID(context.Background(), cfg3, fakeService, cfg3.Folder) + folderID, _, err = r.getOrCreateFolder(context.Background(), cfg3, fakeService, cfg3.Folder) require.NoError(t, err) identity = dashboardIdentity{folderID: folderID, title: "Grafana"}