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
pull/45150/head
Serge Zaitsev 3 years ago committed by GitHub
parent 49ac8b9f0a
commit a231c6861c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      pkg/api/api.go
  2. 12
      pkg/api/org.go
  3. 2
      pkg/services/dashboards/dashboard.go
  4. 14
      pkg/services/dashboards/dashboard_provisioning_mock.go
  5. 3
      pkg/services/dashboards/dashboard_service_mock.go
  6. 26
      pkg/services/dashboards/database/database.go
  7. 14
      pkg/services/dashboards/database/database_mock.go
  8. 5
      pkg/services/dashboards/database/database_provisioning_test.go
  9. 4
      pkg/services/dashboards/manager/dashboard_service.go
  10. 7
      pkg/services/provisioning/dashboards/config_reader.go
  11. 14
      pkg/services/provisioning/dashboards/config_reader_test.go
  12. 14
      pkg/services/provisioning/dashboards/dashboard.go
  13. 6
      pkg/services/provisioning/dashboards/file_reader.go
  14. 11
      pkg/services/provisioning/dashboards/file_reader_test.go
  15. 33
      pkg/services/provisioning/dashboards/validator_test.go
  16. 5
      pkg/services/provisioning/datasources/config_reader.go
  17. 290
      pkg/services/provisioning/datasources/config_reader_test.go
  18. 28
      pkg/services/provisioning/datasources/datasources.go
  19. 35
      pkg/services/provisioning/notifiers/alert_notifications.go
  20. 3
      pkg/services/provisioning/notifiers/config_reader.go
  21. 25
      pkg/services/provisioning/notifiers/config_reader_test.go
  22. 57
      pkg/services/provisioning/plugins/mocks/Store.go
  23. 17
      pkg/services/provisioning/plugins/plugin_provisioner.go
  24. 61
      pkg/services/provisioning/plugins/plugin_provisioner_test.go
  25. 22
      pkg/services/provisioning/provisioning.go
  26. 3
      pkg/services/provisioning/provisioning_test.go
  27. 9
      pkg/services/provisioning/utils/utils.go
  28. 31
      pkg/services/provisioning/utils/utils_test.go
  29. 42
      pkg/services/sqlstore/dashboard_provisioning.go
  30. 15
      pkg/services/sqlstore/mockstore/mockstore.go
  31. 8
      pkg/services/sqlstore/org.go
  32. 1
      pkg/services/sqlstore/sqlstore.go
  33. 4
      pkg/services/sqlstore/stats_test.go
  34. 3
      pkg/services/sqlstore/store.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))

@ -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)
}

@ -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

@ -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)

@ -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
}

@ -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

@ -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)

@ -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)

@ -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)

@ -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)
}

@ -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)

@ -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)
}
}

@ -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
}

@ -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)
})

@ -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"}

@ -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
}

@ -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
}

@ -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
}

@ -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
}
}

@ -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)
}
}

@ -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 {

@ -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
}

@ -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
}
}

@ -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
}

@ -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)
}

@ -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,

@ -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
}

@ -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)
})
})
}

@ -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
}

@ -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
}

@ -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 {

@ -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)

@ -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

@ -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)

Loading…
Cancel
Save