From a231c6861c990d44a49b550df3cdd7edf51fab6e Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Wed, 23 Feb 2022 11:12:37 +0100 Subject: [PATCH] Chore: Remove bus.Dispatch from provisioning services (#44989) * make getordbyname a method * remove one dispatch from plugins provisioner * remove bus from the plugins provisioner, skip test for now * remove bus from datasource provisioning * resolve tests in notifier provisioning * remove bus from the dashboards provisioning service * fix missing struct field * fix getorgbyid method calls * pass org store into dashboard provisioner * fix test function prototype * fix tests * attempt to fix tests after the rebase * fix integration test * avoid using transaction * remove comments --- pkg/api/api.go | 4 +- pkg/api/org.go | 12 +- pkg/services/dashboards/dashboard.go | 2 + .../dashboards/dashboard_provisioning_mock.go | 14 + .../dashboards/dashboard_service_mock.go | 3 + pkg/services/dashboards/database/database.go | 26 ++ .../dashboards/database/database_mock.go | 14 + .../database/database_provisioning_test.go | 5 +- .../dashboards/manager/dashboard_service.go | 4 + .../provisioning/dashboards/config_reader.go | 7 +- .../dashboards/config_reader_test.go | 14 +- .../provisioning/dashboards/dashboard.go | 14 +- .../provisioning/dashboards/file_reader.go | 6 +- .../dashboards/file_reader_test.go | 11 +- .../provisioning/dashboards/validator_test.go | 33 +- .../provisioning/datasources/config_reader.go | 5 +- .../datasources/config_reader_test.go | 290 ++++++++---------- .../provisioning/datasources/datasources.go | 28 +- .../notifiers/alert_notifications.go | 35 ++- .../provisioning/notifiers/config_reader.go | 3 +- .../notifiers/config_reader_test.go | 25 +- .../provisioning/plugins/mocks/Store.go | 57 ++++ .../plugins/plugin_provisioner.go | 17 +- .../plugins/plugin_provisioner_test.go | 61 ++-- pkg/services/provisioning/provisioning.go | 22 +- .../provisioning/provisioning_test.go | 3 +- pkg/services/provisioning/utils/utils.go | 9 +- pkg/services/provisioning/utils/utils_test.go | 31 -- .../sqlstore/dashboard_provisioning.go | 42 --- pkg/services/sqlstore/mockstore/mockstore.go | 15 +- pkg/services/sqlstore/org.go | 8 +- pkg/services/sqlstore/sqlstore.go | 1 - pkg/services/sqlstore/stats_test.go | 4 +- pkg/services/sqlstore/store.go | 3 +- 34 files changed, 448 insertions(+), 380 deletions(-) create mode 100644 pkg/services/provisioning/plugins/mocks/Store.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 02aeb599393..8f352f87300 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -204,7 +204,7 @@ func (hs *HTTPServer) registerRoutes() { // org information available to all users. apiRoute.Group("/org", func(orgRoute routing.RouteRegister) { - orgRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsRead)), routing.Wrap(GetCurrentOrg)) + orgRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsRead)), routing.Wrap(hs.GetCurrentOrg)) orgRoute.Get("/quotas", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsQuotasRead)), routing.Wrap(hs.GetCurrentOrgQuotas)) }) @@ -243,7 +243,7 @@ func (hs *HTTPServer) registerRoutes() { // orgs (admin routes) apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) { userIDScope := ac.Scope("users", "id", ac.Parameter(":userId")) - orgsRoute.Get("/", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsRead)), routing.Wrap(GetOrgByID)) + orgsRoute.Get("/", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsRead)), routing.Wrap(hs.GetOrgByID)) orgsRoute.Put("/", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsWrite)), routing.Wrap(hs.UpdateOrg)) orgsRoute.Put("/address", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsWrite)), routing.Wrap(hs.UpdateOrgAddress)) orgsRoute.Delete("/", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsDelete)), routing.Wrap(hs.DeleteOrgByID)) diff --git a/pkg/api/org.go b/pkg/api/org.go index fa2b4ce944f..21abb8fe5f0 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -18,17 +18,17 @@ import ( ) // GET /api/org -func GetCurrentOrg(c *models.ReqContext) response.Response { - return getOrgHelper(c.Req.Context(), c.OrgId) +func (hs *HTTPServer) GetCurrentOrg(c *models.ReqContext) response.Response { + return hs.getOrgHelper(c.Req.Context(), c.OrgId) } // GET /api/orgs/:orgId -func GetOrgByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgByID(c *models.ReqContext) response.Response { orgId, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "orgId is invalid", err) } - return getOrgHelper(c.Req.Context(), orgId) + return hs.getOrgHelper(c.Req.Context(), orgId) } // GET /api/orgs/name/:name @@ -57,10 +57,10 @@ func (hs *HTTPServer) GetOrgByName(c *models.ReqContext) response.Response { return response.JSON(200, &result) } -func getOrgHelper(ctx context.Context, orgID int64) response.Response { +func (hs *HTTPServer) getOrgHelper(ctx context.Context, orgID int64) response.Response { query := models.GetOrgByIdQuery{Id: orgID} - if err := sqlstore.GetOrgById(ctx, &query); err != nil { + if err := hs.SQLStore.GetOrgById(ctx, &query); err != nil { if errors.Is(err, models.ErrOrgNotFound) { return response.Error(404, "Organization not found", err) } diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index f3cca22bfe4..da1273ffa9f 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -26,6 +26,7 @@ type DashboardProvisioningService interface { GetProvisionedDashboardDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) UnprovisionDashboard(ctx context.Context, dashboardID int64) error DeleteProvisionedDashboard(ctx context.Context, dashboardID int64, orgID int64) error + DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error } //go:generate mockery --name Store --structname FakeDashboardStore --output database --outpkg database --filename database_mock.go @@ -41,6 +42,7 @@ type Store interface { SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error + DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error // SaveAlerts saves dashboard alerts. SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error UnprovisionDashboard(ctx context.Context, id int64) error diff --git a/pkg/services/dashboards/dashboard_provisioning_mock.go b/pkg/services/dashboards/dashboard_provisioning_mock.go index 46ae5a09745..92da6bb4ef5 100644 --- a/pkg/services/dashboards/dashboard_provisioning_mock.go +++ b/pkg/services/dashboards/dashboard_provisioning_mock.go @@ -14,6 +14,20 @@ type FakeDashboardProvisioning struct { mock.Mock } +// DeleteOrphanedProvisionedDashboards provides a mock function with given fields: ctx, cmd +func (_m *FakeDashboardProvisioning) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error { + ret := _m.Called(ctx, cmd) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.DeleteOrphanedProvisionedDashboardsCommand) error); ok { + r0 = rf(ctx, cmd) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteProvisionedDashboard provides a mock function with given fields: ctx, dashboardID, orgID func (_m *FakeDashboardProvisioning) DeleteProvisionedDashboard(ctx context.Context, dashboardID int64, orgID int64) error { ret := _m.Called(ctx, dashboardID, orgID) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 9d6331879cb..307641e981e 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -42,3 +42,6 @@ func (s *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId func (s *FakeDashboardService) GetProvisionedDashboardDataByDashboardID(id int64) (*models.DashboardProvisioning, error) { return s.ProvisionedDashData, nil } +func (s *FakeDashboardService) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error { + return nil +} diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 60b0b44d5f1..611935c2ab3 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -2,6 +2,7 @@ package database import ( "context" + "errors" "fmt" "time" @@ -195,6 +196,31 @@ func (d *DashboardStore) UnprovisionDashboard(ctx context.Context, id int64) err }) } +func (d *DashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error { + return d.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + var result []*models.DashboardProvisioning + + convertedReaderNames := make([]interface{}, len(cmd.ReaderNames)) + for index, readerName := range cmd.ReaderNames { + convertedReaderNames[index] = readerName + } + + err := sess.NotIn("name", convertedReaderNames...).Find(&result) + if err != nil { + return err + } + + for _, deleteDashCommand := range result { + err := d.sqlStore.DeleteDashboard(ctx, &models.DeleteDashboardCommand{Id: deleteDashCommand.DashboardId}) + if err != nil && !errors.Is(err, models.ErrDashboardNotFound) { + return err + } + } + + return nil + }) +} + func getExistingDashboardByIdOrUidForUpdate(sess *sqlstore.DBSession, dash *models.Dashboard, dialect migrator.Dialect, overwrite bool) (bool, error) { dashWithIdExists := false isParentFolderChanged := false diff --git a/pkg/services/dashboards/database/database_mock.go b/pkg/services/dashboards/database/database_mock.go index c8b154f970f..8091583a48c 100644 --- a/pkg/services/dashboards/database/database_mock.go +++ b/pkg/services/dashboards/database/database_mock.go @@ -15,6 +15,20 @@ type FakeDashboardStore struct { mock.Mock } +// DeleteOrphanedProvisionedDashboards provides a mock function with given fields: ctx, cmd +func (_m *FakeDashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error { + ret := _m.Called(ctx, cmd) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.DeleteOrphanedProvisionedDashboardsCommand) error); ok { + r0 = rf(ctx, cmd) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // GetFolderByTitle provides a mock function with given fields: orgID, title func (_m *FakeDashboardStore) GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error) { ret := _m.Called(orgID, title) diff --git a/pkg/services/dashboards/database/database_provisioning_test.go b/pkg/services/dashboards/database/database_provisioning_test.go index bec37866108..968252d8ce3 100644 --- a/pkg/services/dashboards/database/database_provisioning_test.go +++ b/pkg/services/dashboards/database/database_provisioning_test.go @@ -5,10 +5,11 @@ package database import ( "context" - "github.com/grafana/grafana/pkg/services/sqlstore" "testing" "time" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" @@ -82,7 +83,7 @@ func TestDashboardProvisioningTest(t *testing.T) { require.NotNil(t, query.Result) deleteCmd := &models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}} - require.Nil(t, sqlStore.DeleteOrphanedProvisionedDashboards(context.Background(), deleteCmd)) + require.Nil(t, dashboardStore.DeleteOrphanedProvisionedDashboards(context.Background(), deleteCmd)) query = &models.GetDashboardsQuery{DashboardIds: []int64{dash.Id, anotherDash.Id}} err = sqlStore.GetDashboards(context.Background(), query) diff --git a/pkg/services/dashboards/manager/dashboard_service.go b/pkg/services/dashboards/manager/dashboard_service.go index 759582dfcdd..93457ffa7bc 100644 --- a/pkg/services/dashboards/manager/dashboard_service.go +++ b/pkg/services/dashboards/manager/dashboard_service.go @@ -135,6 +135,10 @@ func (dr *DashboardServiceImpl) UpdateDashboardACL(ctx context.Context, uid int6 return dr.dashboardStore.UpdateDashboardACL(ctx, uid, items) } +func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error { + return dr.dashboardStore.DeleteOrphanedProvisionedDashboards(ctx, cmd) +} + var validateAlerts = func(ctx context.Context, dash *models.Dashboard, user *models.SignedInUser) error { extractor := alerting.NewDashAlertExtractor(dash, dash.OrgId, user) return extractor.ValidateAlerts(ctx) diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index d6dc0c3a2cc..ab65d1f8539 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -14,8 +14,9 @@ import ( ) type configReader struct { - path string - log log.Logger + path string + log log.Logger + orgStore utils.OrgStore } func (cr *configReader) parseConfigs(file os.FileInfo) ([]*config, error) { @@ -93,7 +94,7 @@ func (cr *configReader) readConfig(ctx context.Context) ([]*config, error) { dashboard.OrgID = 1 } - if err := utils.CheckOrgExists(ctx, dashboard.OrgID); err != nil { + if err := utils.CheckOrgExists(ctx, cr.orgStore, dashboard.OrgID); err != nil { return nil, fmt.Errorf("failed to provision dashboards with %q reader: %w", dashboard.Name, err) } diff --git a/pkg/services/provisioning/dashboards/config_reader_test.go b/pkg/services/provisioning/dashboards/config_reader_test.go index 1af40e82853..7d04ae7d2d6 100644 --- a/pkg/services/provisioning/dashboards/config_reader_test.go +++ b/pkg/services/provisioning/dashboards/config_reader_test.go @@ -25,10 +25,10 @@ var ( func TestDashboardsAsConfig(t *testing.T) { t.Run("Dashboards as configuration", func(t *testing.T) { logger := log.New("test-logger") - sqlstore.InitTestDB(t) + store := sqlstore.InitTestDB(t) t.Run("Should fail if orgs don't exist in the database", func(t *testing.T) { - cfgProvider := configReader{path: appliedDefaults, log: logger} + cfgProvider := configReader{path: appliedDefaults, log: logger, orgStore: store} _, err := cfgProvider.readConfig(context.Background()) require.Error(t, err) assert.True(t, errors.Is(err, models.ErrOrgNotFound)) @@ -41,7 +41,7 @@ func TestDashboardsAsConfig(t *testing.T) { } t.Run("default values should be applied", func(t *testing.T) { - cfgProvider := configReader{path: appliedDefaults, log: logger} + cfgProvider := configReader{path: appliedDefaults, log: logger, orgStore: store} cfg, err := cfgProvider.readConfig(context.Background()) require.NoError(t, err) @@ -52,7 +52,7 @@ func TestDashboardsAsConfig(t *testing.T) { t.Run("Can read config file version 1 format", func(t *testing.T) { _ = os.Setenv("TEST_VAR", "general") - cfgProvider := configReader{path: simpleDashboardConfig, log: logger} + cfgProvider := configReader{path: simpleDashboardConfig, log: logger, orgStore: store} cfg, err := cfgProvider.readConfig(context.Background()) _ = os.Unsetenv("TEST_VAR") require.NoError(t, err) @@ -61,7 +61,7 @@ func TestDashboardsAsConfig(t *testing.T) { }) t.Run("Can read config file in version 0 format", func(t *testing.T) { - cfgProvider := configReader{path: oldVersion, log: logger} + cfgProvider := configReader{path: oldVersion, log: logger, orgStore: store} cfg, err := cfgProvider.readConfig(context.Background()) require.NoError(t, err) @@ -69,7 +69,7 @@ func TestDashboardsAsConfig(t *testing.T) { }) t.Run("Should skip invalid path", func(t *testing.T) { - cfgProvider := configReader{path: "/invalid-directory", log: logger} + cfgProvider := configReader{path: "/invalid-directory", log: logger, orgStore: store} cfg, err := cfgProvider.readConfig(context.Background()) if err != nil { t.Fatalf("readConfig return an error %v", err) @@ -79,7 +79,7 @@ func TestDashboardsAsConfig(t *testing.T) { }) t.Run("Should skip broken config files", func(t *testing.T) { - cfgProvider := configReader{path: brokenConfigs, log: logger} + cfgProvider := configReader{path: brokenConfigs, log: logger, orgStore: store} cfg, err := cfgProvider.readConfig(context.Background()) if err != nil { t.Fatalf("readConfig return an error %v", err) diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index 660d33fa8bb..e34c3a0b25e 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -5,10 +5,10 @@ import ( "fmt" "os" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -23,7 +23,7 @@ type DashboardProvisioner interface { } // DashboardProvisionerFactory creates DashboardProvisioners based on input -type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService) (DashboardProvisioner, error) +type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, utils.OrgStore) (DashboardProvisioner, error) // Provisioner is responsible for syncing dashboard from disk to Grafana's database. type Provisioner struct { @@ -31,18 +31,19 @@ type Provisioner struct { fileReaders []*FileReader configs []*config duplicateValidator duplicateValidator + provisioner dashboards.DashboardProvisioningService } // New returns a new DashboardProvisioner -func New(ctx context.Context, configDirectory string, service dashboards.DashboardProvisioningService) (DashboardProvisioner, error) { +func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgStore utils.OrgStore) (DashboardProvisioner, error) { logger := log.New("provisioning.dashboard") - cfgReader := &configReader{path: configDirectory, log: logger} + cfgReader := &configReader{path: configDirectory, log: logger, orgStore: orgStore} configs, err := cfgReader.readConfig(ctx) if err != nil { return nil, errutil.Wrap("Failed to read dashboards config", err) } - fileReaders, err := getFileReaders(configs, logger, service) + fileReaders, err := getFileReaders(configs, logger, provisioner) if err != nil { return nil, errutil.Wrap("Failed to initialize file readers", err) } @@ -52,6 +53,7 @@ func New(ctx context.Context, configDirectory string, service dashboards.Dashboa fileReaders: fileReaders, configs: configs, duplicateValidator: newDuplicateValidator(logger, fileReaders), + provisioner: provisioner, } return d, nil @@ -84,7 +86,7 @@ func (provider *Provisioner) CleanUpOrphanedDashboards(ctx context.Context) { currentReaders[index] = reader.Cfg.Name } - if err := bus.Dispatch(ctx, &models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: currentReaders}); err != nil { + if err := provider.provisioner.DeleteOrphanedProvisionedDashboards(ctx, &models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: currentReaders}); err != nil { provider.log.Warn("Failed to delete orphaned provisioned dashboards", "err", err) } } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 2b84b13dc62..59755ecd14e 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -138,7 +138,7 @@ 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]*models.DashboardProvisioning, usageTracker *usageTracker) error { - folderID, err := getOrCreateFolderID(ctx, fr.Cfg, fr.dashboardProvisioningService, fr.Cfg.Folder) + folderID, err := fr.getOrCreateFolderID(ctx, fr.Cfg, fr.dashboardProvisioningService, fr.Cfg.Folder) if err != nil && !errors.Is(err, ErrFolderNameMissing) { return err } @@ -168,7 +168,7 @@ func (fr *FileReader) storeDashboardsInFoldersFromFileStructure(ctx context.Cont folderName = filepath.Base(dashboardsFolder) } - folderID, err := getOrCreateFolderID(ctx, fr.Cfg, fr.dashboardProvisioningService, folderName) + folderID, err := fr.getOrCreateFolderID(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) } @@ -290,7 +290,7 @@ func getProvisionedDashboardsByPath(service dashboards.DashboardProvisioningServ return byPath, nil } -func getOrCreateFolderID(ctx context.Context, cfg *config, service dashboards.DashboardProvisioningService, folderName string) (int64, error) { +func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, service dashboards.DashboardProvisioningService, folderName string) (int64, error) { if folderName == "" { return 0, ErrFolderNameMissing } diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index 810b3a8d284..1c2def3a342 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -364,8 +364,10 @@ func TestDashboardFileReader(t *testing.T) { "folder": defaultDashboards, }, } + r, err := NewDashboardFileReader(cfg, logger, nil) + require.NoError(t, err) - _, err := getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) + _, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) require.Equal(t, err, ErrFolderNameMissing) }) @@ -380,9 +382,12 @@ func TestDashboardFileReader(t *testing.T) { "folder": defaultDashboards, }, } + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once() + + r, err := NewDashboardFileReader(cfg, logger, nil) + require.NoError(t, err) - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() - _, err := getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) + _, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) require.NoError(t, err) }) diff --git a/pkg/services/provisioning/dashboards/validator_test.go b/pkg/services/provisioning/dashboards/validator_test.go index 040e4699b45..e571ca2dbfe 100644 --- a/pkg/services/provisioning/dashboards/validator_test.go +++ b/pkg/services/provisioning/dashboards/validator_test.go @@ -29,18 +29,18 @@ func TestDuplicatesValidator(t *testing.T) { Type: "file", OrgID: 1, Folder: "", - Options: map[string]interface{}{}, + Options: map[string]interface{}{"path": dashboardContainingUID}, } 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" - - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(3) - fakeService.On("GetProvisionedDashboardData", mock.Anything).Return([]*models.DashboardProvisioning{}, nil).Times(2) - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) - - folderID, err := getOrCreateFolderID(context.Background(), cfg, fakeService, folderName) + r, err := NewDashboardFileReader(cfg, logger, nil) + require.NoError(t, err) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(6) + fakeService.On("GetProvisionedDashboardData", mock.Anything).Return([]*models.DashboardProvisioning{}, nil).Times(4) + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5) + folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, folderName) require.NoError(t, err) identity := dashboardIdentity{folderID: folderID, title: "Grafana"} @@ -89,12 +89,9 @@ func TestDuplicatesValidator(t *testing.T) { t.Run("Duplicates validator should not collect info about duplicate UIDs and titles within folders for different orgs", func(t *testing.T) { const folderName = "duplicates-validator-folder" - - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(3) - fakeService.On("GetProvisionedDashboardData", mock.Anything).Return([]*models.DashboardProvisioning{}, nil).Times(2) - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) - - folderID, err := getOrCreateFolderID(context.Background(), cfg, fakeService, folderName) + r, err := NewDashboardFileReader(cfg, logger, nil) + require.NoError(t, err) + folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, folderName) require.NoError(t, err) identity := dashboardIdentity{folderID: folderID, title: "Grafana"} @@ -154,7 +151,7 @@ func TestDuplicatesValidator(t *testing.T) { t.Run("Duplicates validator should restrict write access only for readers with duplicates", func(t *testing.T) { fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5) fakeService.On("GetProvisionedDashboardData", mock.Anything).Return([]*models.DashboardProvisioning{}, nil).Times(3) - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(6) + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5) cfg1 := &config{ Name: "first", Type: "file", OrgID: 1, Folder: "duplicates-validator-folder", @@ -194,7 +191,9 @@ func TestDuplicatesValidator(t *testing.T) { duplicates := duplicateValidator.getDuplicates() - folderID, err := getOrCreateFolderID(context.Background(), cfg, fakeService, cfg1.Folder) + r, err := NewDashboardFileReader(cfg, logger, nil) + require.NoError(t, err) + folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg1.Folder) require.NoError(t, err) identity := dashboardIdentity{folderID: folderID, title: "Grafana"} @@ -209,7 +208,9 @@ func TestDuplicatesValidator(t *testing.T) { sort.Strings(titleUsageReaders) require.Equal(t, []string{"first"}, titleUsageReaders) - folderID, err = getOrCreateFolderID(context.Background(), cfg3, fakeService, cfg3.Folder) + r, err = NewDashboardFileReader(cfg3, logger, nil) + require.NoError(t, err) + folderID, err = r.getOrCreateFolderID(context.Background(), cfg3, fakeService, cfg3.Folder) require.NoError(t, err) identity = dashboardIdentity{folderID: folderID, title: "Grafana"} diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go index 243c2f726ab..eb1bb03e9cc 100644 --- a/pkg/services/provisioning/datasources/config_reader.go +++ b/pkg/services/provisioning/datasources/config_reader.go @@ -16,7 +16,8 @@ import ( ) type configReader struct { - log log.Logger + log log.Logger + orgStore utils.OrgStore } func (cr *configReader) readConfig(ctx context.Context, path string) ([]*configs, error) { @@ -129,7 +130,7 @@ func (cr *configReader) validateDefaultUniqueness(ctx context.Context, datasourc } func (cr *configReader) validateAccessAndOrgID(ctx context.Context, ds *upsertDataSourceFromConfig) error { - if err := utils.CheckOrgExists(ctx, ds.OrgID); err != nil { + if err := utils.CheckOrgExists(ctx, cr.orgStore, ds.OrgID); err != nil { return err } diff --git a/pkg/services/provisioning/datasources/config_reader_test.go b/pkg/services/provisioning/datasources/config_reader_test.go index 242d97cc9c4..fb0811f540b 100644 --- a/pkg/services/provisioning/datasources/config_reader_test.go +++ b/pkg/services/provisioning/datasources/config_reader_test.go @@ -5,7 +5,6 @@ import ( "os" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" @@ -26,173 +25,133 @@ var ( multipleOrgsWithDefault = "testdata/multiple-org-default" withoutDefaults = "testdata/appliedDefaults" invalidAccess = "testdata/invalid-access" - - fakeRepo *fakeRepository ) func TestDatasourceAsConfig(t *testing.T) { - setup := func() { - fakeRepo = &fakeRepository{} - bus.ClearBusHandlers() - bus.AddHandler("test", mockDelete) - bus.AddHandler("test", mockInsert) - bus.AddHandler("test", mockUpdate) - bus.AddHandler("test", mockGet) - bus.AddHandler("test", mockGetOrg) - } + t.Run("when some values missing should apply default on insert", func(t *testing.T) { + store := &spyStore{} + orgStore := &mockOrgStore{ExpectedOrg: &models.Org{Id: 1}} + dc := newDatasourceProvisioner(logger, store, orgStore) + err := dc.applyChanges(context.Background(), withoutDefaults) + if err != nil { + t.Fatalf("applyChanges return an error %v", err) + } + + require.Equal(t, len(store.inserted), 1) + require.Equal(t, store.inserted[0].OrgId, int64(1)) + require.Equal(t, store.inserted[0].Access, models.DsAccess("proxy")) + require.Equal(t, store.inserted[0].Name, "My datasource name") + require.Equal(t, store.inserted[0].Uid, "P2AD1F727255C56BA") + }) + + t.Run("when some values missing should not change UID when updates", func(t *testing.T) { + store := &spyStore{ + items: []*models.DataSource{{Name: "My datasource name", OrgId: 1, Id: 1, Uid: util.GenerateShortUID()}}, + } + orgStore := &mockOrgStore{} + dc := newDatasourceProvisioner(logger, store, orgStore) + err := dc.applyChanges(context.Background(), withoutDefaults) + if err != nil { + t.Fatalf("applyChanges return an error %v", err) + } - t.Run("when some values missing", func(t *testing.T) { - t.Run("should apply default on insert", func(t *testing.T) { - setup() - dc := newDatasourceProvisioner(logger) - err := dc.applyChanges(context.Background(), withoutDefaults) - if err != nil { - t.Fatalf("applyChanges return an error %v", err) - } - - require.Equal(t, len(fakeRepo.inserted), 1) - require.Equal(t, fakeRepo.inserted[0].OrgId, int64(1)) - require.Equal(t, fakeRepo.inserted[0].Access, models.DsAccess("proxy")) - require.Equal(t, fakeRepo.inserted[0].Name, "My datasource name") - require.Equal(t, fakeRepo.inserted[0].Uid, "P2AD1F727255C56BA") - }) - - t.Run("should not change UID when updates", func(t *testing.T) { - setup() - - fakeRepo.loadAll = []*models.DataSource{ - {Name: "My datasource name", OrgId: 1, Id: 1, Uid: util.GenerateShortUID()}, - } - - dc := newDatasourceProvisioner(logger) - err := dc.applyChanges(context.Background(), withoutDefaults) - if err != nil { - t.Fatalf("applyChanges return an error %v", err) - } - - require.Equal(t, len(fakeRepo.deleted), 0) - require.Equal(t, len(fakeRepo.inserted), 0) - require.Equal(t, len(fakeRepo.updated), 1) - require.Equal(t, "", fakeRepo.updated[0].Uid) // XORM will not update the field if its value is default - }) + require.Equal(t, len(store.deleted), 0) + require.Equal(t, len(store.inserted), 0) + require.Equal(t, len(store.updated), 1) + require.Equal(t, "", store.updated[0].Uid) // XORM will not update the field if its value is default }) t.Run("no datasource in database", func(t *testing.T) { - setup() - dc := newDatasourceProvisioner(logger) + store := &spyStore{} + orgStore := &mockOrgStore{} + dc := newDatasourceProvisioner(logger, store, orgStore) err := dc.applyChanges(context.Background(), twoDatasourcesConfig) if err != nil { t.Fatalf("applyChanges return an error %v", err) } - require.Equal(t, len(fakeRepo.deleted), 0) - require.Equal(t, len(fakeRepo.inserted), 2) - require.Equal(t, len(fakeRepo.updated), 0) + require.Equal(t, len(store.deleted), 0) + require.Equal(t, len(store.inserted), 2) + require.Equal(t, len(store.updated), 0) }) - t.Run("One datasource in database with same name", func(t *testing.T) { - setup() - fakeRepo.loadAll = []*models.DataSource{ - {Name: "Graphite", OrgId: 1, Id: 1}, + t.Run("One datasource in database with same name should update one datasource", func(t *testing.T) { + store := &spyStore{items: []*models.DataSource{{Name: "Graphite", OrgId: 1, Id: 1}}} + orgStore := &mockOrgStore{} + dc := newDatasourceProvisioner(logger, store, orgStore) + err := dc.applyChanges(context.Background(), twoDatasourcesConfig) + if err != nil { + t.Fatalf("applyChanges return an error %v", err) } - t.Run("should update one datasource", func(t *testing.T) { - dc := newDatasourceProvisioner(logger) - err := dc.applyChanges(context.Background(), twoDatasourcesConfig) - if err != nil { - t.Fatalf("applyChanges return an error %v", err) - } - - require.Equal(t, len(fakeRepo.deleted), 0) - require.Equal(t, len(fakeRepo.inserted), 1) - require.Equal(t, len(fakeRepo.updated), 1) - }) + require.Equal(t, len(store.deleted), 0) + require.Equal(t, len(store.inserted), 1) + require.Equal(t, len(store.updated), 1) }) - t.Run("Two datasources with is_default", func(t *testing.T) { - setup() - dc := newDatasourceProvisioner(logger) + t.Run("Two datasources with is_default should raise error", func(t *testing.T) { + store := &spyStore{} + orgStore := &mockOrgStore{} + dc := newDatasourceProvisioner(logger, store, orgStore) err := dc.applyChanges(context.Background(), doubleDatasourcesConfig) - t.Run("should raise error", func(t *testing.T) { require.Equal(t, err, ErrInvalidConfigToManyDefault) }) + require.Equal(t, err, ErrInvalidConfigToManyDefault) }) - t.Run("Multiple datasources in different organizations with isDefault in each organization", func(t *testing.T) { - setup() - dc := newDatasourceProvisioner(logger) + t.Run("Multiple datasources in different organizations with isDefault in each organization should not raise error", func(t *testing.T) { + store := &spyStore{} + orgStore := &mockOrgStore{} + dc := newDatasourceProvisioner(logger, store, orgStore) err := dc.applyChanges(context.Background(), multipleOrgsWithDefault) - t.Run("should not raise error", func(t *testing.T) { - require.NoError(t, err) - require.Equal(t, len(fakeRepo.inserted), 4) - require.True(t, fakeRepo.inserted[0].IsDefault) - require.Equal(t, fakeRepo.inserted[0].OrgId, int64(1)) - require.True(t, fakeRepo.inserted[2].IsDefault) - require.Equal(t, fakeRepo.inserted[2].OrgId, int64(2)) - }) + require.NoError(t, err) + require.Equal(t, len(store.inserted), 4) + require.True(t, store.inserted[0].IsDefault) + require.Equal(t, store.inserted[0].OrgId, int64(1)) + require.True(t, store.inserted[2].IsDefault) + require.Equal(t, store.inserted[2].OrgId, int64(2)) }) - t.Run("Remove one datasource", func(t *testing.T) { - setup() - t.Run("Remove one datasource", func(t *testing.T) { - fakeRepo.loadAll = []*models.DataSource{} - - t.Run("should have removed old datasource", func(t *testing.T) { - dc := newDatasourceProvisioner(logger) - err := dc.applyChanges(context.Background(), deleteOneDatasource) - if err != nil { - t.Fatalf("applyChanges return an error %v", err) - } - - require.Equal(t, 1, len(fakeRepo.deleted)) - // should have set OrgID to 1 - require.Equal(t, fakeRepo.deleted[0].OrgID, int64(1)) - require.Equal(t, 0, len(fakeRepo.inserted)) - require.Equal(t, len(fakeRepo.updated), 0) - }) - }) + t.Run("Remove one datasource should have removed old datasource", func(t *testing.T) { + store := &spyStore{} + orgStore := &mockOrgStore{} + dc := newDatasourceProvisioner(logger, store, orgStore) + err := dc.applyChanges(context.Background(), deleteOneDatasource) + if err != nil { + t.Fatalf("applyChanges return an error %v", err) + } + + require.Equal(t, 1, len(store.deleted)) + // should have set OrgID to 1 + require.Equal(t, store.deleted[0].OrgID, int64(1)) + require.Equal(t, 0, len(store.inserted)) + require.Equal(t, len(store.updated), 0) }) - t.Run("Two configured datasource and purge others ", func(t *testing.T) { - setup() - t.Run("two other datasources in database", func(t *testing.T) { - fakeRepo.loadAll = []*models.DataSource{ - {Name: "old-graphite", OrgId: 1, Id: 1}, - {Name: "old-graphite2", OrgId: 1, Id: 2}, - } - - t.Run("should have two new datasources", func(t *testing.T) { - dc := newDatasourceProvisioner(logger) - err := dc.applyChanges(context.Background(), twoDatasourcesConfigPurgeOthers) - if err != nil { - t.Fatalf("applyChanges return an error %v", err) - } - - require.Equal(t, len(fakeRepo.deleted), 2) - require.Equal(t, len(fakeRepo.inserted), 2) - require.Equal(t, len(fakeRepo.updated), 0) - }) - }) + t.Run("Two configured datasource and purge others", func(t *testing.T) { + store := &spyStore{items: []*models.DataSource{{Name: "old-graphite", OrgId: 1, Id: 1}, {Name: "old-graphite2", OrgId: 1, Id: 2}}} + orgStore := &mockOrgStore{} + dc := newDatasourceProvisioner(logger, store, orgStore) + err := dc.applyChanges(context.Background(), twoDatasourcesConfigPurgeOthers) + if err != nil { + t.Fatalf("applyChanges return an error %v", err) + } + + require.Equal(t, len(store.deleted), 2) + require.Equal(t, len(store.inserted), 2) + require.Equal(t, len(store.updated), 0) }) t.Run("Two configured datasource and purge others = false", func(t *testing.T) { - setup() - t.Run("two other datasources in database", func(t *testing.T) { - fakeRepo.loadAll = []*models.DataSource{ - {Name: "Graphite", OrgId: 1, Id: 1}, - {Name: "old-graphite2", OrgId: 1, Id: 2}, - } - - t.Run("should have two new datasources", func(t *testing.T) { - dc := newDatasourceProvisioner(logger) - err := dc.applyChanges(context.Background(), twoDatasourcesConfig) - if err != nil { - t.Fatalf("applyChanges return an error %v", err) - } - - require.Equal(t, len(fakeRepo.deleted), 0) - require.Equal(t, len(fakeRepo.inserted), 1) - require.Equal(t, len(fakeRepo.updated), 1) - }) - }) + store := &spyStore{items: []*models.DataSource{{Name: "Graphite", OrgId: 1, Id: 1}, {Name: "old-graphite2", OrgId: 1, Id: 2}}} + orgStore := &mockOrgStore{} + dc := newDatasourceProvisioner(logger, store, orgStore) + err := dc.applyChanges(context.Background(), twoDatasourcesConfig) + if err != nil { + t.Fatalf("applyChanges return an error %v", err) + } + + require.Equal(t, len(store.deleted), 0) + require.Equal(t, len(store.inserted), 1) + require.Equal(t, len(store.updated), 1) }) t.Run("broken yaml should return error", func(t *testing.T) { @@ -202,14 +161,14 @@ func TestDatasourceAsConfig(t *testing.T) { }) t.Run("invalid access should warn about invalid value and return 'proxy'", func(t *testing.T) { - reader := &configReader{log: logger} + reader := &configReader{log: logger, orgStore: &mockOrgStore{}} configs, err := reader.readConfig(context.Background(), invalidAccess) require.NoError(t, err) require.Equal(t, configs[0].Datasources[0].Access, models.DS_ACCESS_PROXY) }) t.Run("skip invalid directory", func(t *testing.T) { - cfgProvider := &configReader{log: log.New("test logger")} + cfgProvider := &configReader{log: log.New("test logger"), orgStore: &mockOrgStore{}} cfg, err := cfgProvider.readConfig(context.Background(), "./invalid-directory") if err != nil { t.Fatalf("readConfig return an error %v", err) @@ -220,7 +179,7 @@ func TestDatasourceAsConfig(t *testing.T) { t.Run("can read all properties from version 1", func(t *testing.T) { _ = os.Setenv("TEST_VAR", "name") - cfgProvider := &configReader{log: log.New("test logger")} + cfgProvider := &configReader{log: log.New("test logger"), orgStore: &mockOrgStore{}} cfg, err := cfgProvider.readConfig(context.Background(), allProperties) _ = os.Unsetenv("TEST_VAR") if err != nil { @@ -249,7 +208,7 @@ func TestDatasourceAsConfig(t *testing.T) { }) t.Run("can read all properties from version 0", func(t *testing.T) { - cfgProvider := &configReader{log: log.New("test logger")} + cfgProvider := &configReader{log: log.New("test logger"), orgStore: &mockOrgStore{}} cfg, err := cfgProvider.readConfig(context.Background(), versionZero) if err != nil { t.Fatalf("readConfig return an error %v", err) @@ -308,40 +267,41 @@ func validateDatasourceV1(t *testing.T, dsCfg *configs) { require.Equal(t, ds.UID, "test_uid") } -type fakeRepository struct { +type mockOrgStore struct{ ExpectedOrg *models.Org } + +func (m *mockOrgStore) GetOrgById(c context.Context, cmd *models.GetOrgByIdQuery) error { + cmd.Result = m.ExpectedOrg + return nil +} + +type spyStore struct { inserted []*models.AddDataSourceCommand deleted []*models.DeleteDataSourceCommand updated []*models.UpdateDataSourceCommand - - loadAll []*models.DataSource + items []*models.DataSource } -func mockDelete(ctx context.Context, cmd *models.DeleteDataSourceCommand) error { - fakeRepo.deleted = append(fakeRepo.deleted, cmd) - return nil +func (s *spyStore) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error { + for _, v := range s.items { + if query.Name == v.Name && query.OrgId == v.OrgId { + query.Result = v + return nil + } + } + return models.ErrDataSourceNotFound } -func mockUpdate(ctx context.Context, cmd *models.UpdateDataSourceCommand) error { - fakeRepo.updated = append(fakeRepo.updated, cmd) +func (s *spyStore) DeleteDataSource(ctx context.Context, cmd *models.DeleteDataSourceCommand) error { + s.deleted = append(s.deleted, cmd) return nil } -func mockInsert(ctx context.Context, cmd *models.AddDataSourceCommand) error { - fakeRepo.inserted = append(fakeRepo.inserted, cmd) +func (s *spyStore) AddDataSource(ctx context.Context, cmd *models.AddDataSourceCommand) error { + s.inserted = append(s.inserted, cmd) return nil } -func mockGet(ctx context.Context, cmd *models.GetDataSourceQuery) error { - for _, v := range fakeRepo.loadAll { - if cmd.Name == v.Name && cmd.OrgId == v.OrgId { - cmd.Result = v - return nil - } - } - - return models.ErrDataSourceNotFound -} - -func mockGetOrg(ctx context.Context, _ *models.GetOrgByIdQuery) error { +func (s *spyStore) UpdateDataSource(ctx context.Context, cmd *models.UpdateDataSourceCommand) error { + s.updated = append(s.updated, cmd) return nil } diff --git a/pkg/services/provisioning/datasources/datasources.go b/pkg/services/provisioning/datasources/datasources.go index 329192343a4..2845b09d2e6 100644 --- a/pkg/services/provisioning/datasources/datasources.go +++ b/pkg/services/provisioning/datasources/datasources.go @@ -4,13 +4,19 @@ import ( "context" "errors" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/models" ) +type Store interface { + GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error + AddDataSource(ctx context.Context, cmd *models.AddDataSourceCommand) error + UpdateDataSource(ctx context.Context, cmd *models.UpdateDataSourceCommand) error + DeleteDataSource(ctx context.Context, cmd *models.DeleteDataSourceCommand) error +} + var ( // ErrInvalidConfigToManyDefault indicates that multiple datasource in the provisioning files // contains more than one datasource marked as default. @@ -19,8 +25,8 @@ var ( // Provision scans a directory for provisioning config files // and provisions the datasource in those files. -func Provision(ctx context.Context, configDirectory string) error { - dc := newDatasourceProvisioner(log.New("provisioning.datasources")) +func Provision(ctx context.Context, configDirectory string, store Store, orgStore utils.OrgStore) error { + dc := newDatasourceProvisioner(log.New("provisioning.datasources"), store, orgStore) return dc.applyChanges(ctx, configDirectory) } @@ -29,12 +35,14 @@ func Provision(ctx context.Context, configDirectory string) error { type DatasourceProvisioner struct { log log.Logger cfgProvider *configReader + store Store } -func newDatasourceProvisioner(log log.Logger) DatasourceProvisioner { +func newDatasourceProvisioner(log log.Logger, store Store, orgStore utils.OrgStore) DatasourceProvisioner { return DatasourceProvisioner{ log: log, - cfgProvider: &configReader{log: log}, + cfgProvider: &configReader{log: log, orgStore: orgStore}, + store: store, } } @@ -45,7 +53,7 @@ func (dc *DatasourceProvisioner) apply(ctx context.Context, cfg *configs) error for _, ds := range cfg.Datasources { cmd := &models.GetDataSourceQuery{OrgId: ds.OrgID, Name: ds.Name} - err := bus.Dispatch(ctx, cmd) + err := dc.store.GetDataSource(ctx, cmd) if err != nil && !errors.Is(err, models.ErrDataSourceNotFound) { return err } @@ -53,13 +61,13 @@ func (dc *DatasourceProvisioner) apply(ctx context.Context, cfg *configs) error if errors.Is(err, models.ErrDataSourceNotFound) { insertCmd := createInsertCommand(ds) dc.log.Info("inserting datasource from configuration ", "name", insertCmd.Name, "uid", insertCmd.Uid) - if err := bus.Dispatch(ctx, insertCmd); err != nil { + if err := dc.store.AddDataSource(ctx, insertCmd); err != nil { return err } } else { updateCmd := createUpdateCommand(ds, cmd.Result.Id) dc.log.Debug("updating datasource from configuration", "name", updateCmd.Name, "uid", updateCmd.Uid) - if err := bus.Dispatch(ctx, updateCmd); err != nil { + if err := dc.store.UpdateDataSource(ctx, updateCmd); err != nil { return err } } @@ -86,7 +94,7 @@ func (dc *DatasourceProvisioner) applyChanges(ctx context.Context, configPath st func (dc *DatasourceProvisioner) deleteDatasources(ctx context.Context, dsToDelete []*deleteDatasourceConfig) error { for _, ds := range dsToDelete { cmd := &models.DeleteDataSourceCommand{OrgID: ds.OrgID, Name: ds.Name} - if err := bus.Dispatch(ctx, cmd); err != nil { + if err := dc.store.DeleteDataSource(ctx, cmd); err != nil { return err } diff --git a/pkg/services/provisioning/notifiers/alert_notifications.go b/pkg/services/provisioning/notifiers/alert_notifications.go index dff39292842..3195837c72b 100644 --- a/pkg/services/provisioning/notifiers/alert_notifications.go +++ b/pkg/services/provisioning/notifiers/alert_notifications.go @@ -1,7 +1,6 @@ package notifiers import ( - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/encryption" @@ -9,9 +8,18 @@ import ( "golang.org/x/net/context" ) +type Store interface { + GetOrgById(c context.Context, cmd *models.GetOrgByIdQuery) error + GetOrgByNameHandler(ctx context.Context, query *models.GetOrgByNameQuery) error + GetAlertNotificationsWithUid(ctx context.Context, query *models.GetAlertNotificationsWithUidQuery) error + DeleteAlertNotificationWithUid(ctx context.Context, cmd *models.DeleteAlertNotificationWithUidCommand) error + CreateAlertNotificationCommand(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error + UpdateAlertNotificationWithUid(ctx context.Context, cmd *models.UpdateAlertNotificationWithUidCommand) error +} + // Provision alert notifiers -func Provision(ctx context.Context, configDirectory string, encryptionService encryption.Internal, notificationService *notifications.NotificationService) error { - dc := newNotificationProvisioner(encryptionService, notificationService, log.New("provisioning.notifiers")) +func Provision(ctx context.Context, configDirectory string, store Store, encryptionService encryption.Internal, notificationService *notifications.NotificationService) error { + dc := newNotificationProvisioner(store, encryptionService, notificationService, log.New("provisioning.notifiers")) return dc.applyChanges(ctx, configDirectory) } @@ -19,15 +27,18 @@ func Provision(ctx context.Context, configDirectory string, encryptionService en type NotificationProvisioner struct { log log.Logger cfgProvider *configReader + store Store } -func newNotificationProvisioner(encryptionService encryption.Internal, notifiationService *notifications.NotificationService, log log.Logger) NotificationProvisioner { +func newNotificationProvisioner(store Store, encryptionService encryption.Internal, notifiationService *notifications.NotificationService, log log.Logger) NotificationProvisioner { return NotificationProvisioner{ - log: log, + log: log, + store: store, cfgProvider: &configReader{ encryptionService: encryptionService, notificationService: notifiationService, log: log, + orgStore: store, }, } } @@ -50,7 +61,7 @@ func (dc *NotificationProvisioner) deleteNotifications(ctx context.Context, noti if notification.OrgID == 0 && notification.OrgName != "" { getOrg := &models.GetOrgByNameQuery{Name: notification.OrgName} - if err := bus.Dispatch(ctx, getOrg); err != nil { + if err := dc.store.GetOrgByNameHandler(ctx, getOrg); err != nil { return err } notification.OrgID = getOrg.Result.Id @@ -60,13 +71,13 @@ func (dc *NotificationProvisioner) deleteNotifications(ctx context.Context, noti getNotification := &models.GetAlertNotificationsWithUidQuery{Uid: notification.UID, OrgId: notification.OrgID} - if err := bus.Dispatch(ctx, getNotification); err != nil { + if err := dc.store.GetAlertNotificationsWithUid(ctx, getNotification); err != nil { return err } if getNotification.Result != nil { cmd := &models.DeleteAlertNotificationWithUidCommand{Uid: getNotification.Result.Uid, OrgId: getNotification.OrgId} - if err := bus.Dispatch(ctx, cmd); err != nil { + if err := dc.store.DeleteAlertNotificationWithUid(ctx, cmd); err != nil { return err } } @@ -79,7 +90,7 @@ func (dc *NotificationProvisioner) mergeNotifications(ctx context.Context, notif for _, notification := range notificationToMerge { if notification.OrgID == 0 && notification.OrgName != "" { getOrg := &models.GetOrgByNameQuery{Name: notification.OrgName} - if err := bus.Dispatch(ctx, getOrg); err != nil { + if err := dc.store.GetOrgByNameHandler(ctx, getOrg); err != nil { return err } notification.OrgID = getOrg.Result.Id @@ -88,7 +99,7 @@ func (dc *NotificationProvisioner) mergeNotifications(ctx context.Context, notif } cmd := &models.GetAlertNotificationsWithUidQuery{OrgId: notification.OrgID, Uid: notification.UID} - err := bus.Dispatch(ctx, cmd) + err := dc.store.GetAlertNotificationsWithUid(ctx, cmd) if err != nil { return err } @@ -108,7 +119,7 @@ func (dc *NotificationProvisioner) mergeNotifications(ctx context.Context, notif SendReminder: notification.SendReminder, } - if err := bus.Dispatch(ctx, insertCmd); err != nil { + if err := dc.store.CreateAlertNotificationCommand(ctx, insertCmd); err != nil { return err } } else { @@ -126,7 +137,7 @@ func (dc *NotificationProvisioner) mergeNotifications(ctx context.Context, notif SendReminder: notification.SendReminder, } - if err := bus.Dispatch(ctx, updateCmd); err != nil { + if err := dc.store.UpdateAlertNotificationWithUid(ctx, updateCmd); err != nil { return err } } diff --git a/pkg/services/provisioning/notifiers/config_reader.go b/pkg/services/provisioning/notifiers/config_reader.go index f5680db8cc3..b502c3872e7 100644 --- a/pkg/services/provisioning/notifiers/config_reader.go +++ b/pkg/services/provisioning/notifiers/config_reader.go @@ -21,6 +21,7 @@ import ( type configReader struct { encryptionService encryption.Internal notificationService *notifications.NotificationService + orgStore utils.OrgStore log log.Logger } @@ -93,7 +94,7 @@ func (cr *configReader) checkOrgIDAndOrgName(ctx context.Context, notifications notification.OrgID = 0 } } else { - if err := utils.CheckOrgExists(ctx, notification.OrgID); err != nil { + if err := utils.CheckOrgExists(ctx, cr.orgStore, notification.OrgID); err != nil { return fmt.Errorf("failed to provision %q notification: %w", notification.Name, err) } } diff --git a/pkg/services/provisioning/notifiers/config_reader_test.go b/pkg/services/provisioning/notifiers/config_reader_test.go index 984421265ce..7e8a0dd760b 100644 --- a/pkg/services/provisioning/notifiers/config_reader_test.go +++ b/pkg/services/provisioning/notifiers/config_reader_test.go @@ -62,6 +62,7 @@ func TestNotificationAsConfig(t *testing.T) { setup() _ = os.Setenv("TEST_VAR", "default") cfgProvider := &configReader{ + orgStore: sqlStore, encryptionService: ossencryption.ProvideService(), log: log.New("test logger"), } @@ -139,7 +140,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("One configured notification", func(t *testing.T) { t.Run("no notification in database", func(t *testing.T) { setup() - dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), twoNotificationsConfig) if err != nil { @@ -170,7 +171,7 @@ func TestNotificationAsConfig(t *testing.T) { require.Equal(t, len(notificationsQuery.Result), 1) t.Run("should update one notification", func(t *testing.T) { - dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, ossencryption.ProvideService(), nil, logger) err = dc.applyChanges(context.Background(), twoNotificationsConfig) if err != nil { t.Fatalf("applyChanges return an error %v", err) @@ -194,7 +195,7 @@ func TestNotificationAsConfig(t *testing.T) { }) t.Run("Two notifications with is_default", func(t *testing.T) { setup() - dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), doubleNotificationsConfig) t.Run("should both be inserted", func(t *testing.T) { require.NoError(t, err) @@ -237,7 +238,7 @@ func TestNotificationAsConfig(t *testing.T) { require.Equal(t, len(notificationsQuery.Result), 2) t.Run("should have two new notifications", func(t *testing.T) { - dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), twoNotificationsConfig) if err != nil { t.Fatalf("applyChanges return an error %v", err) @@ -254,11 +255,11 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("Can read correct properties with orgName instead of orgId", func(t *testing.T) { setup() existingOrg1 := models.GetOrgByNameQuery{Name: "Main Org. 1"} - err := sqlstore.GetOrgByName(context.Background(), &existingOrg1) + err := sqlStore.GetOrgByNameHandler(context.Background(), &existingOrg1) require.NoError(t, err) require.NotNil(t, existingOrg1.Result) existingOrg2 := models.GetOrgByNameQuery{Name: "Main Org. 2"} - err = sqlstore.GetOrgByName(context.Background(), &existingOrg2) + err = sqlStore.GetOrgByNameHandler(context.Background(), &existingOrg2) require.NoError(t, err) require.NotNil(t, existingOrg2.Result) @@ -271,7 +272,7 @@ func TestNotificationAsConfig(t *testing.T) { err = sqlStore.CreateAlertNotificationCommand(context.Background(), &existingNotificationCmd) require.NoError(t, err) - dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, ossencryption.ProvideService(), nil, logger) err = dc.applyChanges(context.Background(), correctPropertiesWithOrgName) if err != nil { t.Fatalf("applyChanges return an error %v", err) @@ -290,7 +291,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("Config doesn't contain required field", func(t *testing.T) { setup() - dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), noRequiredFields) require.NotNil(t, err) @@ -304,7 +305,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("Empty yaml file", func(t *testing.T) { t.Run("should have not changed repo", func(t *testing.T) { setup() - dc := newNotificationProvisioner(ossencryption.ProvideService(), nil, logger) + dc := newNotificationProvisioner(sqlStore, ossencryption.ProvideService(), nil, logger) err := dc.applyChanges(context.Background(), emptyFile) if err != nil { t.Fatalf("applyChanges return an error %v", err) @@ -318,6 +319,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("Broken yaml should return error", func(t *testing.T) { reader := &configReader{ + orgStore: sqlStore, encryptionService: ossencryption.ProvideService(), log: log.New("test logger"), } @@ -328,6 +330,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("Skip invalid directory", func(t *testing.T) { cfgProvider := &configReader{ + orgStore: sqlStore, encryptionService: ossencryption.ProvideService(), log: log.New("test logger"), } @@ -341,6 +344,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("Unknown notifier should return error", func(t *testing.T) { cfgProvider := &configReader{ + orgStore: sqlStore, encryptionService: ossencryption.ProvideService(), log: log.New("test logger"), } @@ -351,6 +355,7 @@ func TestNotificationAsConfig(t *testing.T) { t.Run("Read incorrect properties", func(t *testing.T) { cfgProvider := &configReader{ + orgStore: sqlStore, encryptionService: ossencryption.ProvideService(), log: log.New("test logger"), } @@ -363,7 +368,7 @@ func TestNotificationAsConfig(t *testing.T) { func setupBusHandlers(sqlStore *sqlstore.SQLStore) { bus.AddHandler("getOrg", func(ctx context.Context, q *models.GetOrgByNameQuery) error { - return sqlstore.GetOrgByName(ctx, q) + return sqlStore.GetOrgByNameHandler(ctx, q) }) bus.AddHandler("getAlertNotifications", func(ctx context.Context, q *models.GetAlertNotificationsWithUidQuery) error { diff --git a/pkg/services/provisioning/plugins/mocks/Store.go b/pkg/services/provisioning/plugins/mocks/Store.go new file mode 100644 index 00000000000..c32d458b1b4 --- /dev/null +++ b/pkg/services/provisioning/plugins/mocks/Store.go @@ -0,0 +1,57 @@ +// Code generated by mockery v2.10.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + models "github.com/grafana/grafana/pkg/models" + mock "github.com/stretchr/testify/mock" +) + +// Store is an autogenerated mock type for the Store type +type Store struct { + mock.Mock +} + +// GetOrgByNameHandler provides a mock function with given fields: ctx, query +func (_m *Store) GetOrgByNameHandler(ctx context.Context, query *models.GetOrgByNameQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.GetOrgByNameQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetPluginSettingById provides a mock function with given fields: ctx, query +func (_m *Store) GetPluginSettingById(ctx context.Context, query *models.GetPluginSettingByIdQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.GetPluginSettingByIdQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdatePluginSetting provides a mock function with given fields: ctx, cmd +func (_m *Store) UpdatePluginSetting(ctx context.Context, cmd *models.UpdatePluginSettingCmd) error { + ret := _m.Called(ctx, cmd) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.UpdatePluginSettingCmd) error); ok { + r0 = rf(ctx, cmd) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/services/provisioning/plugins/plugin_provisioner.go b/pkg/services/provisioning/plugins/plugin_provisioner.go index 0093f0baddd..d2ae059f0e3 100644 --- a/pkg/services/provisioning/plugins/plugin_provisioner.go +++ b/pkg/services/provisioning/plugins/plugin_provisioner.go @@ -4,19 +4,25 @@ import ( "context" "errors" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ) +type Store interface { + GetOrgByNameHandler(ctx context.Context, query *models.GetOrgByNameQuery) error + GetPluginSettingById(ctx context.Context, query *models.GetPluginSettingByIdQuery) error + UpdatePluginSetting(ctx context.Context, cmd *models.UpdatePluginSettingCmd) error +} + // Provision scans a directory for provisioning config files // and provisions the app in those files. -func Provision(ctx context.Context, configDirectory string, pluginStore plugins.Store) error { +func Provision(ctx context.Context, configDirectory string, store Store, pluginStore plugins.Store) error { logger := log.New("provisioning.plugins") ap := PluginProvisioner{ log: logger, cfgProvider: newConfigReader(logger, pluginStore), + store: store, } return ap.applyChanges(ctx, configDirectory) } @@ -26,13 +32,14 @@ func Provision(ctx context.Context, configDirectory string, pluginStore plugins. type PluginProvisioner struct { log log.Logger cfgProvider configReader + store Store } func (ap *PluginProvisioner) apply(ctx context.Context, cfg *pluginsAsConfig) error { for _, app := range cfg.Apps { if app.OrgID == 0 && app.OrgName != "" { getOrgQuery := &models.GetOrgByNameQuery{Name: app.OrgName} - if err := bus.Dispatch(ctx, getOrgQuery); err != nil { + if err := ap.store.GetOrgByNameHandler(ctx, getOrgQuery); err != nil { return err } app.OrgID = getOrgQuery.Result.Id @@ -41,7 +48,7 @@ func (ap *PluginProvisioner) apply(ctx context.Context, cfg *pluginsAsConfig) er } query := &models.GetPluginSettingByIdQuery{OrgId: app.OrgID, PluginId: app.PluginID} - err := bus.Dispatch(ctx, query) + err := ap.store.GetPluginSettingById(ctx, query) if err != nil { if !errors.Is(err, models.ErrPluginSettingNotFound) { return err @@ -60,7 +67,7 @@ func (ap *PluginProvisioner) apply(ctx context.Context, cfg *pluginsAsConfig) er SecureJsonData: app.SecureJSONData, PluginVersion: app.PluginVersion, } - if err := bus.Dispatch(ctx, cmd); err != nil { + if err := ap.store.UpdatePluginSetting(ctx, cmd); err != nil { return err } } diff --git a/pkg/services/provisioning/plugins/plugin_provisioner_test.go b/pkg/services/provisioning/plugins/plugin_provisioner_test.go index 98a30436cb5..3c3dff3c8fb 100644 --- a/pkg/services/provisioning/plugins/plugin_provisioner_test.go +++ b/pkg/services/provisioning/plugins/plugin_provisioner_test.go @@ -5,7 +5,6 @@ import ( "errors" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/stretchr/testify/require" @@ -21,32 +20,6 @@ func TestPluginProvisioner(t *testing.T) { }) t.Run("Should apply configurations", func(t *testing.T) { - bus.AddHandler("test", func(ctx context.Context, query *models.GetOrgByNameQuery) error { - if query.Name == "Org 4" { - query.Result = &models.Org{Id: 4} - } - - return nil - }) - - bus.AddHandler("test", func(ctx context.Context, query *models.GetPluginSettingByIdQuery) error { - if query.PluginId == "test-plugin" && query.OrgId == 2 { - query.Result = &models.PluginSetting{ - PluginVersion: "2.0.1", - } - return nil - } - - return models.ErrPluginSettingNotFound - }) - - sentCommands := []*models.UpdatePluginSettingCmd{} - - bus.AddHandler("test", func(ctx context.Context, cmd *models.UpdatePluginSettingCmd) error { - sentCommands = append(sentCommands, cmd) - return nil - }) - cfg := []*pluginsAsConfig{ { Apps: []*appFromConfig{ @@ -58,11 +31,12 @@ func TestPluginProvisioner(t *testing.T) { }, } reader := &testConfigReader{result: cfg} - ap := PluginProvisioner{log: log.New("test"), cfgProvider: reader} + store := &mockStore{} + ap := PluginProvisioner{log: log.New("test"), cfgProvider: reader, store: store} err := ap.applyChanges(context.Background(), "") require.NoError(t, err) - require.Len(t, sentCommands, 4) + require.Len(t, store.sentCommands, 4) testCases := []struct { ExpectedPluginID string @@ -77,7 +51,7 @@ func TestPluginProvisioner(t *testing.T) { } for index, tc := range testCases { - cmd := sentCommands[index] + cmd := store.sentCommands[index] require.NotNil(t, cmd) require.Equal(t, tc.ExpectedPluginID, cmd.PluginId) require.Equal(t, tc.ExpectedOrgID, cmd.OrgId) @@ -95,3 +69,30 @@ type testConfigReader struct { func (tcr *testConfigReader) readConfig(ctx context.Context, path string) ([]*pluginsAsConfig, error) { return tcr.result, tcr.err } + +type mockStore struct { + sentCommands []*models.UpdatePluginSettingCmd +} + +func (m *mockStore) GetOrgByNameHandler(ctx context.Context, query *models.GetOrgByNameQuery) error { + if query.Name == "Org 4" { + query.Result = &models.Org{Id: 4} + } + return nil +} + +func (m *mockStore) GetPluginSettingById(ctx context.Context, query *models.GetPluginSettingByIdQuery) error { + if query.PluginId == "test-plugin" && query.OrgId == 2 { + query.Result = &models.PluginSetting{ + PluginVersion: "2.0.1", + } + return nil + } + + return models.ErrPluginSettingNotFound +} + +func (m *mockStore) UpdatePluginSetting(ctx context.Context, cmd *models.UpdatePluginSettingCmd) error { + m.sentCommands = append(m.sentCommands, cmd) + return nil +} diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 711b90b7962..2621fac3eb8 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/services/provisioning/datasources" "github.com/grafana/grafana/pkg/services/provisioning/notifiers" "github.com/grafana/grafana/pkg/services/provisioning/plugins" + "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" @@ -26,6 +27,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p ) (*ProvisioningServiceImpl, error) { s := &ProvisioningServiceImpl{ Cfg: cfg, + SQLStore: sqlStore, pluginStore: pluginStore, EncryptionService: encryptionService, NotificationService: notificatonService, @@ -64,9 +66,9 @@ func NewProvisioningServiceImpl() *ProvisioningServiceImpl { // Used for testing purposes func newProvisioningServiceImpl( newDashboardProvisioner dashboards.DashboardProvisionerFactory, - provisionNotifiers func(context.Context, string, encryption.Internal, *notifications.NotificationService) error, - provisionDatasources func(context.Context, string) error, - provisionPlugins func(context.Context, string, plugifaces.Store) error, + provisionNotifiers func(context.Context, string, notifiers.Store, encryption.Internal, *notifications.NotificationService) error, + provisionDatasources func(context.Context, string, datasources.Store, utils.OrgStore) error, + provisionPlugins func(context.Context, string, plugins.Store, plugifaces.Store) error, ) *ProvisioningServiceImpl { return &ProvisioningServiceImpl{ log: log.New("provisioning"), @@ -87,9 +89,9 @@ type ProvisioningServiceImpl struct { pollingCtxCancel context.CancelFunc newDashboardProvisioner dashboards.DashboardProvisionerFactory dashboardProvisioner dashboards.DashboardProvisioner - provisionNotifiers func(context.Context, string, encryption.Internal, *notifications.NotificationService) error - provisionDatasources func(context.Context, string) error - provisionPlugins func(context.Context, string, plugifaces.Store) error + provisionNotifiers func(context.Context, string, notifiers.Store, encryption.Internal, *notifications.NotificationService) error + provisionDatasources func(context.Context, string, datasources.Store, utils.OrgStore) error + provisionPlugins func(context.Context, string, plugins.Store, plugifaces.Store) error mutex sync.Mutex dashboardService dashboardservice.DashboardProvisioningService } @@ -144,7 +146,7 @@ func (ps *ProvisioningServiceImpl) Run(ctx context.Context) error { func (ps *ProvisioningServiceImpl) ProvisionDatasources(ctx context.Context) error { datasourcePath := filepath.Join(ps.Cfg.ProvisioningPath, "datasources") - if err := ps.provisionDatasources(ctx, datasourcePath); err != nil { + if err := ps.provisionDatasources(ctx, datasourcePath, ps.SQLStore, ps.SQLStore); err != nil { err = errutil.Wrap("Datasource provisioning error", err) ps.log.Error("Failed to provision data sources", "error", err) return err @@ -154,7 +156,7 @@ func (ps *ProvisioningServiceImpl) ProvisionDatasources(ctx context.Context) err func (ps *ProvisioningServiceImpl) ProvisionPlugins(ctx context.Context) error { appPath := filepath.Join(ps.Cfg.ProvisioningPath, "plugins") - if err := ps.provisionPlugins(ctx, appPath, ps.pluginStore); err != nil { + if err := ps.provisionPlugins(ctx, appPath, ps.SQLStore, ps.pluginStore); err != nil { err = errutil.Wrap("app provisioning error", err) ps.log.Error("Failed to provision plugins", "error", err) return err @@ -164,7 +166,7 @@ func (ps *ProvisioningServiceImpl) ProvisionPlugins(ctx context.Context) error { func (ps *ProvisioningServiceImpl) ProvisionNotifications(ctx context.Context) error { alertNotificationsPath := filepath.Join(ps.Cfg.ProvisioningPath, "notifiers") - if err := ps.provisionNotifiers(ctx, alertNotificationsPath, ps.EncryptionService, ps.NotificationService); err != nil { + if err := ps.provisionNotifiers(ctx, alertNotificationsPath, ps.SQLStore, ps.EncryptionService, ps.NotificationService); err != nil { err = errutil.Wrap("Alert notification provisioning error", err) ps.log.Error("Failed to provision alert notifications", "error", err) return err @@ -174,7 +176,7 @@ func (ps *ProvisioningServiceImpl) ProvisionNotifications(ctx context.Context) e func (ps *ProvisioningServiceImpl) ProvisionDashboards(ctx context.Context) error { dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards") - dashProvisioner, err := ps.newDashboardProvisioner(ctx, dashboardPath, ps.dashboardService) + dashProvisioner, err := ps.newDashboardProvisioner(ctx, dashboardPath, ps.dashboardService, ps.SQLStore) if err != nil { return errutil.Wrap("Failed to create provisioner", err) } diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go index 8bf248f97fa..4493d6569dd 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -8,6 +8,7 @@ import ( dashboardstore "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" + "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" ) @@ -92,7 +93,7 @@ func setup() *serviceTestStruct { } serviceTest.service = newProvisioningServiceImpl( - func(context.Context, string, dashboardstore.DashboardProvisioningService) (dashboards.DashboardProvisioner, error) { + func(context.Context, string, dashboardstore.DashboardProvisioningService, utils.OrgStore) (dashboards.DashboardProvisioner, error) { return serviceTest.mock, nil }, nil, diff --git a/pkg/services/provisioning/utils/utils.go b/pkg/services/provisioning/utils/utils.go index 13c270c5442..18f74408b52 100644 --- a/pkg/services/provisioning/utils/utils.go +++ b/pkg/services/provisioning/utils/utils.go @@ -5,13 +5,16 @@ import ( "errors" "fmt" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ) -func CheckOrgExists(ctx context.Context, orgID int64) error { +type OrgStore interface { + GetOrgById(context.Context, *models.GetOrgByIdQuery) error +} + +func CheckOrgExists(ctx context.Context, store OrgStore, orgID int64) error { query := models.GetOrgByIdQuery{Id: orgID} - if err := bus.Dispatch(ctx, &query); err != nil { + if err := store.GetOrgById(ctx, &query); err != nil { if errors.Is(err, models.ErrOrgNotFound) { return err } diff --git a/pkg/services/provisioning/utils/utils_test.go b/pkg/services/provisioning/utils/utils_test.go index 33b5710bb45..d4b585bf785 100644 --- a/pkg/services/provisioning/utils/utils_test.go +++ b/pkg/services/provisioning/utils/utils_test.go @@ -1,32 +1 @@ package utils - -import ( - "context" - "testing" - - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/sqlstore" - - "github.com/stretchr/testify/require" -) - -func TestCheckOrgExists(t *testing.T) { - t.Run("with default org in database", func(t *testing.T) { - sqlstore.InitTestDB(t) - - defaultOrg := models.CreateOrgCommand{Name: "Main Org."} - - err := sqlstore.CreateOrg(context.Background(), &defaultOrg) - require.NoError(t, err) - - t.Run("default org exists", func(t *testing.T) { - err := CheckOrgExists(context.Background(), defaultOrg.Result.Id) - require.NoError(t, err) - }) - - t.Run("other org doesn't exist", func(t *testing.T) { - err := CheckOrgExists(context.Background(), defaultOrg.Result.Id+1) - require.Equal(t, err, models.ErrOrgNotFound) - }) - }) -} diff --git a/pkg/services/sqlstore/dashboard_provisioning.go b/pkg/services/sqlstore/dashboard_provisioning.go index cc46473caa3..f79e0bcddbe 100644 --- a/pkg/services/sqlstore/dashboard_provisioning.go +++ b/pkg/services/sqlstore/dashboard_provisioning.go @@ -1,43 +1 @@ package sqlstore - -import ( - "context" - "errors" - - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/models" -) - -func (ss *SQLStore) addDashboardProvisioningQueryAndCommandHandlers() { - bus.AddHandler("sql", ss.DeleteOrphanedProvisionedDashboards) -} - -type DashboardExtras struct { - Id int64 - DashboardId int64 - Key string - Value string -} - -func (ss *SQLStore) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error { - var result []*models.DashboardProvisioning - - convertedReaderNames := make([]interface{}, len(cmd.ReaderNames)) - for index, readerName := range cmd.ReaderNames { - convertedReaderNames[index] = readerName - } - - err := x.NotIn("name", convertedReaderNames...).Find(&result) - if err != nil { - return err - } - - for _, deleteDashCommand := range result { - err := ss.DeleteDashboard(ctx, &models.DeleteDashboardCommand{Id: deleteDashCommand.DashboardId}) - if err != nil && !errors.Is(err, models.ErrDashboardNotFound) { - return err - } - } - - return nil -} diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go index 58387874d51..61b19b0a53c 100644 --- a/pkg/services/sqlstore/mockstore/mockstore.go +++ b/pkg/services/sqlstore/mockstore/mockstore.go @@ -13,14 +13,16 @@ type OrgListResponse []struct { Response error } type SQLStoreMock struct { - LastGetAlertsQuery *models.GetAlertsQuery - LatestUserId int64 + LastGetAlertsQuery *models.GetAlertsQuery + LatestUserId int64 + ExpectedUser *models.User ExpectedDatasource *models.DataSource ExpectedAlert *models.Alert ExpectedPluginSetting *models.PluginSetting ExpectedDashboard *models.Dashboard ExpectedDashboards []*models.Dashboard + ExpectedDashboardVersion *models.DashboardVersion ExpectedDashboardVersions []*models.DashboardVersion ExpectedDashboardAclInfoList []*models.DashboardAclInfoDTO ExpectedUserOrgList []*models.UserOrgDTO @@ -92,10 +94,19 @@ func (m *SQLStoreMock) SearchDashboardSnapshots(query *models.GetDashboardSnapsh return m.ExpectedError } +func (m *SQLStoreMock) GetOrgById(ctx context.Context, cmd *models.GetOrgByIdQuery) error { + return m.ExpectedError +} + func (m *SQLStoreMock) GetOrgByName(name string) (*models.Org, error) { return m.ExpectedOrg, m.ExpectedError } +func (m *SQLStoreMock) GetOrgByNameHandler(ctx context.Context, query *models.GetOrgByNameQuery) error { + query.Result = m.ExpectedOrg + return m.ExpectedError +} + func (m *SQLStoreMock) CreateOrgWithMember(name string, userID int64) (models.Org, error) { return *m.ExpectedOrg, nil } diff --git a/pkg/services/sqlstore/org.go b/pkg/services/sqlstore/org.go index a373b81fe5b..e8e538913b1 100644 --- a/pkg/services/sqlstore/org.go +++ b/pkg/services/sqlstore/org.go @@ -16,11 +16,11 @@ import ( const MainOrgName = "Main Org." func (ss *SQLStore) addOrgQueryAndCommandHandlers() { - bus.AddHandler("sql", GetOrgById) + bus.AddHandler("sql", ss.GetOrgById) bus.AddHandler("sql", CreateOrg) bus.AddHandler("sql", ss.UpdateOrg) bus.AddHandler("sql", ss.UpdateOrgAddress) - bus.AddHandler("sql", GetOrgByName) + bus.AddHandler("sql", ss.GetOrgByNameHandler) bus.AddHandler("sql", ss.SearchOrgs) bus.AddHandler("sql", ss.DeleteOrg) } @@ -48,7 +48,7 @@ func (ss *SQLStore) SearchOrgs(ctx context.Context, query *models.SearchOrgsQuer return err } -func GetOrgById(ctx context.Context, query *models.GetOrgByIdQuery) error { +func (ss *SQLStore) GetOrgById(ctx context.Context, query *models.GetOrgByIdQuery) error { var org models.Org exists, err := x.Id(query.Id).Get(&org) if err != nil { @@ -63,7 +63,7 @@ func GetOrgById(ctx context.Context, query *models.GetOrgByIdQuery) error { return nil } -func GetOrgByName(ctx context.Context, query *models.GetOrgByNameQuery) error { +func (ss *SQLStore) GetOrgByNameHandler(ctx context.Context, query *models.GetOrgByNameQuery) error { var org models.Org exists, err := x.Where("name=?", query.Name).Get(&org) if err != nil { diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index b376f6eb0e2..0c50d28161c 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -130,7 +130,6 @@ func newSQLStore(cfg *setting.Cfg, cacheService *localcache.CacheService, b bus. ss.addPlaylistQueryAndCommandHandlers() ss.addLoginAttemptQueryAndCommandHandlers() ss.addTeamQueryAndCommandHandlers() - ss.addDashboardProvisioningQueryAndCommandHandlers() ss.addOrgQueryAndCommandHandlers() bus.AddHandler("sql", ss.GetDBHealthQuery) diff --git a/pkg/services/sqlstore/stats_test.go b/pkg/services/sqlstore/stats_test.go index bd0a67c778d..5a8087559df 100644 --- a/pkg/services/sqlstore/stats_test.go +++ b/pkg/services/sqlstore/stats_test.go @@ -79,7 +79,7 @@ func populateDB(t *testing.T, sqlStore *SQLStore) { // get 1st user's organisation getOrgByIdQuery := &models.GetOrgByIdQuery{Id: users[0].OrgId} - err := GetOrgById(context.Background(), getOrgByIdQuery) + err := sqlStore.GetOrgById(context.Background(), getOrgByIdQuery) require.NoError(t, err) org := getOrgByIdQuery.Result @@ -103,7 +103,7 @@ func populateDB(t *testing.T, sqlStore *SQLStore) { // get 2nd user's organisation getOrgByIdQuery = &models.GetOrgByIdQuery{Id: users[1].OrgId} - err = GetOrgById(context.Background(), getOrgByIdQuery) + err = sqlStore.GetOrgById(context.Background(), getOrgByIdQuery) require.NoError(t, err) org = getOrgByIdQuery.Result diff --git a/pkg/services/sqlstore/store.go b/pkg/services/sqlstore/store.go index 100b3e75f21..82c6f104820 100644 --- a/pkg/services/sqlstore/store.go +++ b/pkg/services/sqlstore/store.go @@ -24,7 +24,8 @@ type Store interface { UpdateOrg(ctx context.Context, cmd *models.UpdateOrgCommand) error UpdateOrgAddress(ctx context.Context, cmd *models.UpdateOrgAddressCommand) error DeleteOrg(ctx context.Context, cmd *models.DeleteOrgCommand) error - DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error + GetOrgById(context.Context, *models.GetOrgByIdQuery) error + GetOrgByNameHandler(ctx context.Context, query *models.GetOrgByNameQuery) error CreateLoginAttempt(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error DeleteOldLoginAttempts(ctx context.Context, cmd *models.DeleteOldLoginAttemptsCommand) error CloneUserToServiceAccount(ctx context.Context, siUser *models.SignedInUser) (*models.User, error)