From d5b98772edaf1d20dd9fd83779ed03df489852b7 Mon Sep 17 00:00:00 2001 From: Selene Date: Wed, 16 Feb 2022 14:15:44 +0100 Subject: [PATCH] Dashboards: Refactor service to make it injectable by wire (#44588) * Add providers to folder and dashboard services * Refactor folder and dashboard services * Move store implementation to its own file due wire cannot allow us to cast to SQLStore * Add store in some places and more missing dependencies * Bad merge fix * Remove old functions from tests and few fixes * Fix provisioning * Remove store from http server and some test fixes * Test fixes * Fix dashboard and folder tests * Fix library tests * Fix provisioning tests * Fix plugins manager tests * Fix alert and org users tests * Refactor service package and more test fixes * Fix dashboard_test tets * Fix api tests * Some lint fixes * Fix lint * More lint :/ * Move dashboard integration tests to dashboards service and fix dependencies * Lint + tests * More integration tests fixes * Lint * Lint again * Fix tests again and again anda again * Update searchstore_test * Fix goimports * More go imports * More imports fixes * Fix lint * Move UnprovisionDashboard function into dashboard service and remove bus * Use search service instead of bus * Fix test * Fix go imports * Use nil in tests --- pkg/api/acl.go | 15 - pkg/api/common_test.go | 21 +- pkg/api/dashboard.go | 24 +- pkg/api/dashboard_permission.go | 2 +- pkg/api/dashboard_permission_test.go | 74 +-- pkg/api/dashboard_test.go | 145 ++--- pkg/api/folder.go | 23 +- pkg/api/folder_permission.go | 9 +- pkg/api/folder_permission_test.go | 115 +--- pkg/api/folder_test.go | 78 +-- pkg/api/http_server.go | 10 +- pkg/api/org_users_test.go | 4 +- pkg/dashboards/ifaces.go | 23 - pkg/models/dashboards.go | 4 - pkg/plugins/manager/dashboards_test.go | 6 +- pkg/plugins/manager/manager.go | 37 +- .../manager/manager_integration_test.go | 9 +- pkg/plugins/manager/manager_test.go | 9 +- pkg/server/wire.go | 10 + .../dashboardimport/service/service.go | 7 +- pkg/services/dashboards/acl_service.go | 51 -- pkg/services/dashboards/dashboard.go | 47 ++ .../dashboards/dashboard_provisioning_mock.go | 158 +++++ .../dashboards/dashboard_service_mock.go | 44 ++ pkg/services/dashboards/database/database.go | 611 ++++++++++++++++++ .../database/database_dashboard_test.go} | 185 +++--- .../database/database_folder_test.go} | 121 ++-- .../dashboards/database/database_mock.go | 217 +++++++ .../database/database_provisioning_test.go} | 28 +- .../permissions/database_acl_test.go} | 88 ++- pkg/services/dashboards/folder.go | 20 + .../dashboards/folder_service_mock.go | 181 ++++++ .../{ => manager}/dashboard_service.go | 191 +++--- .../dashboard_service_integration_test.go | 30 +- .../{ => manager}/dashboard_service_test.go | 187 ++---- .../{ => manager}/folder_service.go | 106 +-- .../{ => manager}/folder_service_test.go | 72 +-- pkg/services/dashboards/models.go | 16 + pkg/services/libraryelements/guard.go | 5 +- .../libraryelements/libraryelements.go | 5 +- .../libraryelements/libraryelements_test.go | 37 +- .../librarypanels/librarypanels_test.go | 42 +- pkg/services/ngalert/ngalert.go | 11 +- pkg/services/ngalert/store/alert_rule.go | 8 +- pkg/services/ngalert/store/database.go | 2 + pkg/services/ngalert/tests/util.go | 6 +- .../provisioning/dashboards/dashboard.go | 2 +- .../provisioning/dashboards/file_reader.go | 6 +- .../dashboards/file_reader_test.go | 346 +++------- .../provisioning/dashboards/validator_test.go | 30 +- pkg/services/provisioning/provisioning.go | 8 +- .../provisioning/provisioning_test.go | 4 +- pkg/services/sqlstore/dashboard.go | 323 +-------- pkg/services/sqlstore/dashboard_acl.go | 35 - .../sqlstore/dashboard_provisioning.go | 92 --- .../sqlstore/dashboard_version_test.go | 54 +- pkg/services/sqlstore/mockstore/mockstore.go | 53 +- pkg/services/sqlstore/org_test.go | 102 ++- .../sqlstore/searchstore/search_test.go | 31 +- pkg/services/sqlstore/sqlbuilder_test.go | 4 +- pkg/services/sqlstore/store.go | 10 - pkg/services/sqlstore/team_test.go | 2 +- pkg/services/sqlstore/user_test.go | 4 +- .../api/alerting/api_alertmanager_test.go | 9 +- pkg/tests/api/alerting/api_prometheus_test.go | 6 +- pkg/tests/api/alerting/api_ruler_test.go | 6 +- 66 files changed, 2377 insertions(+), 1844 deletions(-) delete mode 100644 pkg/api/acl.go delete mode 100644 pkg/dashboards/ifaces.go delete mode 100644 pkg/services/dashboards/acl_service.go create mode 100644 pkg/services/dashboards/dashboard.go create mode 100644 pkg/services/dashboards/dashboard_provisioning_mock.go create mode 100644 pkg/services/dashboards/dashboard_service_mock.go create mode 100644 pkg/services/dashboards/database/database.go rename pkg/services/{sqlstore/dashboard_test.go => dashboards/database/database_dashboard_test.go} (84%) rename pkg/services/{sqlstore/dashboard_folder_test.go => dashboards/database/database_folder_test.go} (81%) create mode 100644 pkg/services/dashboards/database/database_mock.go rename pkg/services/{sqlstore/dashboard_provisioning_test.go => dashboards/database/database_provisioning_test.go} (79%) rename pkg/services/{sqlstore/dashboard_acl_test.go => dashboards/database/permissions/database_acl_test.go} (72%) create mode 100644 pkg/services/dashboards/folder.go create mode 100644 pkg/services/dashboards/folder_service_mock.go rename pkg/services/dashboards/{ => manager}/dashboard_service.go (60%) rename pkg/services/dashboards/{ => manager}/dashboard_service_integration_test.go (96%) rename pkg/services/dashboards/{ => manager}/dashboard_service_test.go (59%) rename pkg/services/dashboards/{ => manager}/folder_service.go (55%) rename pkg/services/dashboards/{ => manager}/folder_service_test.go (73%) create mode 100644 pkg/services/dashboards/models.go diff --git a/pkg/api/acl.go b/pkg/api/acl.go deleted file mode 100644 index eae0cf21aeb..00000000000 --- a/pkg/api/acl.go +++ /dev/null @@ -1,15 +0,0 @@ -package api - -import ( - "context" - - "github.com/grafana/grafana/pkg/dashboards" - "github.com/grafana/grafana/pkg/models" -) - -// updateDashboardACL updates a dashboard's ACL items. -// -// Stubbable by tests. -var updateDashboardACL = func(ctx context.Context, s dashboards.Store, dashID int64, items []*models.DashboardAcl) error { - return s.UpdateDashboardACLCtx(ctx, dashID, items) -} diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index ce94ba3e77e..a4775311c80 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -28,6 +28,9 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/resourceservices" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/services/dashboards" + dashboardsstore "github.com/grafana/grafana/pkg/services/dashboards/database" + dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/quota" @@ -276,6 +279,8 @@ type accessControlScenarioContext struct { // cfg is the setting provider cfg *setting.Cfg + + dashboardsStore dashboards.Store } func setAccessControlPermissions(acmock *accesscontrolmock.Mock, perms []*accesscontrol.Permission, org int64) { @@ -347,6 +352,8 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont bus := bus.GetBus() + dashboardsStore := dashboardsstore.ProvideDashboardStore(db) + routeRegister := routing.NewRouteRegister() // Create minimal HTTP Server hs := &HTTPServer{ @@ -358,6 +365,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont RouteRegister: routeRegister, SQLStore: db, searchUsersService: searchusers.ProvideUsersService(bus, filters.ProvideOSSSearchUserFilter()), + dashboardService: dashboardservice.ProvideDashboardService(dashboardsStore), } // Defining the accesscontrol service has to be done before registering routes @@ -402,12 +410,13 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont hs.RouteRegister.Register(m.Router) return accessControlScenarioContext{ - server: m, - initCtx: initCtx, - hs: hs, - acmock: acmock, - db: db, - cfg: cfg, + server: m, + initCtx: initCtx, + hs: hs, + acmock: acmock, + db: db, + cfg: cfg, + dashboardsStore: dashboardsStore, } } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 45a5b577ba8..8568a8b4615 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -148,8 +148,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { meta.FolderUrl = query.Result.GetUrl() } - svc := dashboards.NewProvisioningService(hs.SQLStore) - provisioningData, err := svc.GetProvisionedDashboardDataByDashboardID(dash.Id) + provisioningData, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(dash.Id) if err != nil { return response.Error(500, "Error while checking if dashboard is provisioned", err) } @@ -233,8 +232,8 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { if err != nil { hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err) } - svc := dashboards.NewService(hs.SQLStore) - err = svc.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgId) + + err = hs.dashboardService.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgId) if err != nil { var dashboardErr models.DashboardErr if ok := errors.As(err, &dashboardErr); ok { @@ -271,8 +270,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa cmd.OrgId = c.OrgId cmd.UserId = c.UserId if cmd.FolderUid != "" { - folders := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) - folder, err := folders.GetFolderByUID(ctx, cmd.FolderUid) + folder, err := hs.folderService.GetFolderByUID(ctx, c.SignedInUser, c.OrgId, cmd.FolderUid) if err != nil { if errors.Is(err, models.ErrFolderNotFound) { return response.Error(400, "Folder not found", err) @@ -294,18 +292,17 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa } } - svc := dashboards.NewProvisioningService(hs.SQLStore) var provisioningData *models.DashboardProvisioning if dash.Id != 0 { - data, err := svc.GetProvisionedDashboardDataByDashboardID(dash.Id) + data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(dash.Id) if err != nil { return response.Error(500, "Error while checking if dashboard is provisioned using ID", err) } provisioningData = data } else if dash.Uid != "" { - data, err := svc.GetProvisionedDashboardDataByDashboardUID(dash.OrgId, dash.Uid) - if err != nil && (!errors.Is(err, models.ErrProvisionedDashboardNotFound) && !errors.Is(err, models.ErrDashboardNotFound)) { - return response.Error(500, "Error while checking if dashboard is provisioned using UID", err) + data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardUID(dash.OrgId, dash.Uid) + if err != nil && !errors.Is(err, models.ErrProvisionedDashboardNotFound) && !errors.Is(err, models.ErrDashboardNotFound) { + return response.Error(500, "Error while checking if dashboard is provisioned", err) } provisioningData = data } @@ -329,8 +326,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa Overwrite: cmd.Overwrite, } - dashSvc := dashboards.NewService(hs.SQLStore) - dashboard, err := dashSvc.SaveDashboard(alerting.WithUAEnabled(ctx, hs.Cfg.UnifiedAlerting.IsEnabled()), dashItem, allowUiUpdate) + dashboard, err := hs.dashboardService.SaveDashboard(alerting.WithUAEnabled(ctx, hs.Cfg.UnifiedAlerting.IsEnabled()), dashItem, allowUiUpdate) if hs.Live != nil { // Tell everyone listening that the dashboard changed @@ -362,7 +358,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa if hs.Cfg.EditorsCanAdmin && newDashboard { inFolder := cmd.FolderId > 0 - err := dashSvc.MakeUserAdmin(ctx, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder) + err := hs.dashboardService.MakeUserAdmin(ctx, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder) if err != nil { hs.log.Error("Could not make user admin", "dashboard", dashboard.Title, "user", c.SignedInUser.UserId, "error", err) } diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 275acc82d13..da6833dc395 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -112,7 +112,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response. return response.Error(403, "Cannot remove own admin permission for a folder", nil) } - if err := updateDashboardACL(c.Req.Context(), hs.SQLStore, dashID, items); err != nil { + if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), dashID, items); err != nil { if errors.Is(err, models.ErrDashboardAclInfoMissing) || errors.Is(err, models.ErrDashboardPermissionDashboardEmpty) { return response.Error(409, err.Error(), err) diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index f5dd0f490b0..a64ef0b8da3 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -1,59 +1,38 @@ package api import ( - "context" "encoding/json" "fmt" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards/database" + dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) func TestDashboardPermissionAPIEndpoint(t *testing.T) { t.Run("Dashboard permissions test", func(t *testing.T) { settings := setting.NewCfg() + dashboardStore := &database.FakeDashboardStore{} + defer dashboardStore.AssertExpectations(t) + mockSQLStore := mockstore.NewSQLStoreMock() + hs := &HTTPServer{ - Cfg: settings, - SQLStore: mockSQLStore, + Cfg: settings, + dashboardService: dashboardservice.ProvideDashboardService(dashboardStore), + SQLStore: mockSQLStore, } - t.Run("Given dashboard not exists", func(t *testing.T) { - mockSQLStore.ExpectedError = models.ErrDashboardNotFound - loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions", - "/api/dashboards/id/:dashboardId/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) { - callGetDashboardPermissions(sc, hs) - assert.Equal(t, 404, sc.resp.Code) - }, mockSQLStore) - - cmd := dtos.UpdateDashboardAclCommand{ - Items: []dtos.DashboardAclUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN}, - }, - } - - updateDashboardPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/dashboards/id/1/permissions", - routePattern: "/api/dashboards/id/:dashboardId/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateDashboardPermissions(t, sc) - assert.Equal(t, 404, sc.resp.Code) - }, - }, hs) - }) - t.Run("Given user has no admin permissions", func(t *testing.T) { origNewGuardian := guardian.New t.Cleanup(func() { @@ -63,7 +42,6 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false}) getDashboardQueryResult := models.NewDashboard("Dash") - mockSQLStore := mockstore.NewSQLStoreMock() mockSQLStore.ExpectedDashboard = getDashboardQueryResult mockSQLStore.ExpectedError = nil @@ -80,6 +58,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { }, } + dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() updateDashboardPermissionScenario(t, updatePermissionContext{ desc: "When calling POST on", url: "/api/dashboards/id/1/permissions", @@ -324,26 +303,20 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { } assert.Len(t, cmd.Items, 3) + var numOfItems []*models.DashboardAcl + dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + items := args.Get(2).([]*models.DashboardAcl) + numOfItems = items + }).Return(nil).Once() updateDashboardPermissionScenario(t, updatePermissionContext{ desc: "When calling POST on", url: "/api/dashboards/id/1/permissions", routePattern: "/api/dashboards/id/:dashboardId/permissions", cmd: cmd, fn: func(sc *scenarioContext) { - // TODO: Replace this fake with a fake SQLStore instead (once we can use an interface in its stead) - origUpdateDashboardACL := updateDashboardACL - t.Cleanup(func() { - updateDashboardACL = origUpdateDashboardACL - }) - var gotItems []*models.DashboardAcl - updateDashboardACL = func(_ context.Context, _ dashboards.Store, folderID int64, items []*models.DashboardAcl) error { - gotItems = items - return nil - } - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) - assert.Len(t, gotItems, 4) + assert.Len(t, numOfItems, 4) }, }, hs) }) @@ -357,15 +330,6 @@ func callGetDashboardPermissions(sc *scenarioContext, hs *HTTPServer) { func callUpdateDashboardPermissions(t *testing.T, sc *scenarioContext) { t.Helper() - - origUpdateDashboardACL := updateDashboardACL - t.Cleanup(func() { - updateDashboardACL = origUpdateDashboardACL - }) - updateDashboardACL = func(_ context.Context, _ dashboards.Store, dashID int64, items []*models.DashboardAcl) error { - return nil - } - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 3c4f21ec618..01f87d54b0a 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -7,7 +7,6 @@ import ( "fmt" "io/ioutil" "net/http" - "path/filepath" "testing" "github.com/grafana/grafana/pkg/api/dtos" @@ -15,11 +14,12 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" - dboards "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/database" + service "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/live" @@ -30,6 +30,7 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -145,7 +146,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUp() sc.sqlStore = mockSQLStore - dash := getDashboardShouldReturn200(sc) + dash := getDashboardShouldReturn200(t, sc) assert.False(t, dash.Meta.CanEdit) assert.False(t, dash.Meta.CanSave) @@ -177,7 +178,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUp() sc.sqlStore = mockSQLStore - dash := getDashboardShouldReturn200(sc) + dash := getDashboardShouldReturn200(t, sc) assert.True(t, dash.Meta.CanEdit) assert.True(t, dash.Meta.CanSave) @@ -212,11 +213,13 @@ func TestDashboardAPIEndpoint(t *testing.T) { mockSQLStore := mockstore.NewSQLStoreMock() mockSQLStore.ExpectedDashboard = fakeDash + dashboardStore := database.ProvideDashboardStore(sqlstore.InitTestDB(t)) hs := &HTTPServer{ Cfg: setting.NewCfg(), Live: newTestLive(t), LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &mockLibraryElementService{}, + dashboardService: service.ProvideDashboardService(dashboardStore), SQLStore: mockSQLStore, } hs.SQLStore = mockSQLStore @@ -345,7 +348,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUpInner() sc.sqlStore = mockSQLStore - dash := getDashboardShouldReturn200(sc) + dash := getDashboardShouldReturn200(t, sc) assert.True(t, dash.Meta.CanEdit) assert.True(t, dash.Meta.CanSave) @@ -412,7 +415,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { require.True(t, setting.ViewersCanEdit) sc.sqlStore = mockSQLStore - dash := getDashboardShouldReturn200(sc) + dash := getDashboardShouldReturn200(t, sc) assert.True(t, dash.Meta.CanEdit) assert.False(t, dash.Meta.CanSave) @@ -445,7 +448,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUpInner() sc.sqlStore = mockSQLStore - dash := getDashboardShouldReturn200(sc) + dash := getDashboardShouldReturn200(t, sc) assert.True(t, dash.Meta.CanEdit) assert.True(t, dash.Meta.CanSave) @@ -493,7 +496,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUpInner() sc.sqlStore = mockSQLStore - dash := getDashboardShouldReturn200(sc) + dash := getDashboardShouldReturn200(t, sc) assert.False(t, dash.Meta.CanEdit) assert.False(t, dash.Meta.CanSave) @@ -535,6 +538,8 @@ func TestDashboardAPIEndpoint(t *testing.T) { }) t.Run("Post dashboard response tests", func(t *testing.T) { + dashboardStore := &database.FakeDashboardStore{} + defer dashboardStore.AssertExpectations(t) // This tests that a valid request returns correct response t.Run("Given a correct request for creating a dashboard", func(t *testing.T) { const folderID int64 = 3 @@ -562,7 +567,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { }, } - postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, nil, cmd, func(sc *scenarioContext) { + postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, mock, nil, func(sc *scenarioContext) { callPostDashboardShouldReturnSuccess(sc) dto := mock.SavedDashboards[0] @@ -612,7 +617,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { GetFolderByUIDResult: &models.Folder{Id: 1, Uid: "folderUID", Title: "Folder"}, } - postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, mockFolder, cmd, func(sc *scenarioContext) { + postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, mock, mockFolder, func(sc *scenarioContext) { callPostDashboardShouldReturnSuccess(sc) dto := mock.SavedDashboards[0] @@ -661,7 +666,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { GetFolderByUIDError: errors.New("Error while searching Folder ID"), } - postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, mockFolder, cmd, func(sc *scenarioContext) { + postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, mock, mockFolder, func(sc *scenarioContext) { callPostDashboard(sc) assert.Equal(t, 500, sc.resp.Code) }) @@ -706,7 +711,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { } postDashboardScenario(t, fmt.Sprintf("Expect '%s' error when calling POST on", tc.SaveError.Error()), - "/api/dashboards", "/api/dashboards", mock, nil, cmd, func(sc *scenarioContext) { + "/api/dashboards", "/api/dashboards", cmd, mock, nil, func(sc *scenarioContext) { callPostDashboard(sc) assert.Equal(t, tc.ExpectedStatusCode, sc.resp.Code) }) @@ -852,14 +857,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { t.Run("Given provisioned dashboard", func(t *testing.T) { setUp := func() { - origGetProvisionedData := dashboards.GetProvisionedData - t.Cleanup(func() { - dashboards.GetProvisionedData = origGetProvisionedData - }) - dashboards.GetProvisionedData = func(dboards.Store, int64) (*models.DashboardProvisioning, error) { - return &models.DashboardProvisioning{ExternalId: "/tmp/grafana/dashboards/test/dashboard1.json"}, nil - } - bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error { query.Result = []*models.DashboardAclInfoDTO{ {OrgId: testOrgID, DashboardId: 1, UserId: testUserID, Permission: models.PERMISSION_EDIT}, @@ -867,6 +864,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { return nil }) } + mockSQLStore := mockstore.NewSQLStoreMock() dataValue, err := simplejson.NewJson([]byte(`{"id": 1, "editable": true, "style": "dark"}`)) require.NoError(t, err) @@ -874,37 +872,39 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { setUp() - dataValue, err := simplejson.NewJson([]byte(`{"id": 1, "editable": true, "style": "dark"}`)) - require.NoError(t, err) - mockSQLStore.ExpectedDashboard = &models.Dashboard{Id: 1, Data: dataValue} - sc.sqlStore = mockSQLStore - mock := provisioning.NewProvisioningServiceMock(context.Background()) - mock.GetDashboardProvisionerResolvedPathFunc = func(name string) string { + fakeProvisioningService := provisioning.NewProvisioningServiceMock(context.Background()) + fakeProvisioningService.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "/tmp/grafana/dashboards" } - dash := getDashboardShouldReturn200WithConfig(sc, mock) + dashboardStore := &database.FakeDashboardStore{} + defer dashboardStore.AssertExpectations(t) - assert.Equal(t, filepath.Join("test", "dashboard1.json"), dash.Meta.ProvisionedExternalId) + dashboardStore.On("GetProvisionedDataByDashboardID", mock.Anything).Return(&models.DashboardProvisioning{ExternalId: "/dashboard1.json"}, nil).Once() + + dash := getDashboardShouldReturn200WithConfig(t, sc, fakeProvisioningService, dashboardStore) + + assert.Equal(t, "../../../dashboard1.json", dash.Meta.ProvisionedExternalId, mockSQLStore) }, mockSQLStore) loggedInUserScenarioWithRole(t, "When allowUiUpdates is true and calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { setUp() - - mock := provisioning.NewProvisioningServiceMock(context.Background()) - mock.GetDashboardProvisionerResolvedPathFunc = func(name string) string { + fakeProvisioningService := provisioning.NewProvisioningServiceMock(context.Background()) + fakeProvisioningService.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "/tmp/grafana/dashboards" } - mock.GetAllowUIUpdatesFromConfigFunc = func(name string) bool { + + fakeProvisioningService.GetAllowUIUpdatesFromConfigFunc = func(name string) bool { return true } hs := &HTTPServer{ - Cfg: setting.NewCfg(), - ProvisioningService: mock, - LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, - SQLStore: mockSQLStore, + Cfg: setting.NewCfg(), + ProvisioningService: fakeProvisioningService, + LibraryPanelService: &mockLibraryPanelService{}, + LibraryElementService: &mockLibraryElementService{}, + dashboardProvisioningService: mockDashboardProvisioningService{}, + SQLStore: mockSQLStore, } hs.callGetDashboard(sc) @@ -919,21 +919,28 @@ func TestDashboardAPIEndpoint(t *testing.T) { }) } -func getDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningService provisioning.ProvisioningService) dtos. - DashboardFullWithMeta { +func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, provisioningService provisioning.ProvisioningService, dashboardStore dashboards.Store) dtos.DashboardFullWithMeta { + t.Helper() + if provisioningService == nil { provisioningService = provisioning.NewProvisioningServiceMock(context.Background()) } + if dashboardStore == nil { + sql := sqlstore.InitTestDB(t) + dashboardStore = database.ProvideDashboardStore(sql) + } + libraryPanelsService := mockLibraryPanelService{} libraryElementsService := mockLibraryElementService{} hs := &HTTPServer{ - Cfg: setting.NewCfg(), - LibraryPanelService: &libraryPanelsService, - LibraryElementService: &libraryElementsService, - ProvisioningService: provisioningService, - SQLStore: sc.sqlStore, + Cfg: setting.NewCfg(), + LibraryPanelService: &libraryPanelsService, + LibraryElementService: &libraryElementsService, + ProvisioningService: provisioningService, + dashboardProvisioningService: service.ProvideDashboardService(dashboardStore), + SQLStore: sc.sqlStore, } hs.callGetDashboard(sc) @@ -947,8 +954,8 @@ func getDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningServ return dash } -func getDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta { - return getDashboardShouldReturn200WithConfig(sc, nil) +func getDashboardShouldReturn200(t *testing.T, sc *scenarioContext) dtos.DashboardFullWithMeta { + return getDashboardShouldReturn200WithConfig(t, sc, nil, nil) } func (hs *HTTPServer) callGetDashboard(sc *scenarioContext) { @@ -976,17 +983,13 @@ func (hs *HTTPServer) callGetDashboardVersions(sc *scenarioContext) { sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } -func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T, sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) { +func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T, + sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) { bus.AddHandler("test", func(ctx context.Context, cmd *models.DeleteDashboardCommand) error { return nil }) - origNewDashboardService := dashboards.NewService - t.Cleanup(func() { - dashboards.NewService = origNewDashboardService - }) - dashboards.MockDashboardService(mockDashboard) - + hs.dashboardService = mockDashboard sc.handlerFunc = hs.DeleteDashboardByUID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() } @@ -1005,9 +1008,7 @@ func callPostDashboardShouldReturnSuccess(sc *scenarioContext) { assert.Equal(sc.t, 200, sc.resp.Code) } -func postDashboardScenario(t *testing.T, desc string, url string, routePattern string, - mock *dashboards.FakeDashboardService, mockFolder *fakeFolderService, cmd models.SaveDashboardCommand, - fn scenarioFunc) { +func postDashboardScenario(t *testing.T, desc string, url string, routePattern string, cmd models.SaveDashboardCommand, dashboardService dashboards.DashboardService, folderService dashboards.FolderService, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { t.Cleanup(bus.ClearBusHandlers) @@ -1023,6 +1024,8 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s pluginStore: &fakePluginStore{}, LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &mockLibraryElementService{}, + dashboardService: dashboardService, + folderService: folderService, } sc := setupScenarioContext(t, url) @@ -1035,20 +1038,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s return hs.PostDashboard(c) }) - origNewDashboardService := dashboards.NewService - origProvisioningService := dashboards.NewProvisioningService - origNewFolderService := dashboards.NewFolderService - t.Cleanup(func() { - dashboards.NewService = origNewDashboardService - dashboards.NewProvisioningService = origProvisioningService - dashboards.NewFolderService = origNewFolderService - }) - dashboards.MockDashboardService(mock) - dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService { - return mockDashboardProvisioningService{} - } - mockFolderService(mockFolder) - sc.m.Post(routePattern, sc.defaultHandler) fn(sc) @@ -1091,9 +1080,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string }) } -func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string, - mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, - sqlStore sqlstore.Store) { +func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() @@ -1107,6 +1094,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout QuotaService: "a.QuotaService{Cfg: cfg}, LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &mockLibraryElementService{}, + dashboardService: mock, SQLStore: sqlStore, } @@ -1125,17 +1113,6 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout return hs.RestoreDashboardVersion(c) }) - origProvisioningService := dashboards.NewProvisioningService - origNewDashboardService := dashboards.NewService - t.Cleanup(func() { - dashboards.NewService = origNewDashboardService - dashboards.NewProvisioningService = origProvisioningService - }) - dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService { - return mockDashboardProvisioningService{} - } - dashboards.MockDashboardService(mock) - sc.m.Post(routePattern, sc.defaultHandler) fn(sc) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 3fcf762ef89..1c08a0403da 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/util" @@ -19,8 +18,7 @@ import ( ) func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response { - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) - folders, err := s.GetFolders(c.Req.Context(), c.QueryInt64("limit"), c.QueryInt64("page")) + folders, err := hs.folderService.GetFolders(c.Req.Context(), c.SignedInUser, c.OrgId, c.QueryInt64("limit"), c.QueryInt64("page")) if err != nil { return apierrors.ToFolderErrorResponse(err) @@ -40,8 +38,7 @@ func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response { } func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response { - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) - folder, err := s.GetFolderByUID(c.Req.Context(), web.Params(c.Req)[":uid"]) + folder, err := hs.folderService.GetFolderByUID(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"]) if err != nil { return apierrors.ToFolderErrorResponse(err) } @@ -51,14 +48,11 @@ func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response { } func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response { - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) - id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) } - - folder, err := s.GetFolderByID(c.Req.Context(), id) + folder, err := hs.folderService.GetFolderByID(c.Req.Context(), c.SignedInUser, c.OrgId, id) if err != nil { return apierrors.ToFolderErrorResponse(err) } @@ -72,14 +66,13 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response { if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) - folder, err := s.CreateFolder(c.Req.Context(), cmd.Title, cmd.Uid) + folder, err := hs.folderService.CreateFolder(c.Req.Context(), c.SignedInUser, c.OrgId, cmd.Title, cmd.Uid) if err != nil { return apierrors.ToFolderErrorResponse(err) } if hs.Cfg.EditorsCanAdmin { - if err := s.MakeUserAdmin(c.Req.Context(), c.OrgId, c.SignedInUser.UserId, folder.Id, true); err != nil { + if err := hs.folderService.MakeUserAdmin(c.Req.Context(), c.OrgId, c.SignedInUser.UserId, folder.Id, true); err != nil { hs.log.Error("Could not make user admin", "folder", folder.Title, "user", c.SignedInUser.UserId, "error", err) } @@ -94,8 +87,7 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response { if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) - err := s.UpdateFolder(c.Req.Context(), web.Params(c.Req)[":uid"], &cmd) + err := hs.folderService.UpdateFolder(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"], &cmd) if err != nil { return apierrors.ToFolderErrorResponse(err) } @@ -105,7 +97,6 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response { } func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) if err != nil { if errors.Is(err, libraryelements.ErrFolderHasConnectedLibraryElements) { @@ -114,7 +105,7 @@ func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // return apierrors.ToFolderErrorResponse(err) } - f, err := s.DeleteFolder(c.Req.Context(), web.Params(c.Req)[":uid"], c.QueryBool("forceDeleteRules")) + f, err := hs.folderService.DeleteFolder(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"], c.QueryBool("forceDeleteRules")) if err != nil { return apierrors.ToFolderErrorResponse(err) } diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index 3ffd756b3f0..ca03d2497e8 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -9,15 +9,13 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) response.Response { - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) - folder, err := s.GetFolderByUID(c.Req.Context(), web.Params(c.Req)[":uid"]) + folder, err := hs.folderService.GetFolderByUID(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"]) if err != nil { return apierrors.ToFolderErrorResponse(err) @@ -68,8 +66,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res return response.Error(400, err.Error(), err) } - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) - folder, err := s.GetFolderByUID(c.Req.Context(), web.Params(c.Req)[":uid"]) + folder, err := hs.folderService.GetFolderByUID(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"]) if err != nil { return apierrors.ToFolderErrorResponse(err) } @@ -117,7 +114,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res return response.Error(403, "Cannot remove own admin permission for a folder", nil) } - if err := updateDashboardACL(c.Req.Context(), hs.SQLStore, folder.Id, items); err != nil { + if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), folder.Id, items); err != nil { if errors.Is(err, models.ErrDashboardAclInfoMissing) { err = models.ErrFolderAclInfoMissing } diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 395e4a58f2a..46df1f74192 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -1,7 +1,6 @@ package api import ( - "context" "encoding/json" "fmt" "testing" @@ -13,28 +12,28 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" - dashboardifaces "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/database" + service "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/mock" ) func TestFolderPermissionAPIEndpoint(t *testing.T) { settings := setting.NewCfg() - hs := &HTTPServer{Cfg: settings} + folderService := &dashboards.FakeFolderService{} + defer folderService.AssertExpectations(t) + + dashboardStore := &database.FakeDashboardStore{} + defer dashboardStore.AssertExpectations(t) + + hs := &HTTPServer{Cfg: settings, folderService: folderService, dashboardService: service.ProvideDashboardService(dashboardStore)} t.Run("Given folder not exists", func(t *testing.T) { - mock := &fakeFolderService{ - GetFolderByUIDError: models.ErrFolderNotFound, - } - - origNewFolderService := dashboards.NewFolderService - t.Cleanup(func() { - dashboards.NewFolderService = origNewFolderService - }) - mockFolderService(mock) + folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrFolderNotFound).Twice() mockSQLStore := mockstore.NewSQLStoreMock() loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) { callGetFolderPermissions(sc, hs) @@ -61,24 +60,14 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { t.Run("Given user has no admin permissions", func(t *testing.T) { origNewGuardian := guardian.New - origNewFolderService := dashboards.NewFolderService t.Cleanup(func() { guardian.New = origNewGuardian - dashboards.NewFolderService = origNewFolderService }) guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false}) - - mock := &fakeFolderService{ - GetFolderByUIDResult: &models.Folder{ - Id: 1, - Uid: "uid", - Title: "Folder", - }, - } - - mockFolderService(mock) + folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrFolderAccessDenied).Twice() mockSQLStore := mockstore.NewSQLStoreMock() + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) { callGetFolderPermissions(sc, hs) assert.Equal(t, 403, sc.resp.Code) @@ -104,10 +93,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { t.Run("Given user has admin permissions and permissions to update", func(t *testing.T) { origNewGuardian := guardian.New - origNewFolderService := dashboards.NewFolderService t.Cleanup(func() { guardian.New = origNewGuardian - dashboards.NewFolderService = origNewFolderService }) guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ @@ -122,16 +109,11 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { }, }) - mock := &fakeFolderService{ - GetFolderByUIDResult: &models.Folder{ - Id: 1, - Uid: "uid", - Title: "Folder", - }, - } - - mockFolderService(mock) + folderResponse := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"} + folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResponse, nil).Twice() + dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() mockSQLStore := mockstore.NewSQLStoreMock() + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_ADMIN, func(sc *scenarioContext) { callGetFolderPermissions(sc, hs) assert.Equal(t, 200, sc.resp.Code) @@ -175,10 +157,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { t.Run("When trying to update permissions with duplicate permissions", func(t *testing.T) { origNewGuardian := guardian.New - origNewFolderService := dashboards.NewFolderService t.Cleanup(func() { guardian.New = origNewGuardian - dashboards.NewFolderService = origNewFolderService }) guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ @@ -187,15 +167,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists, }) - mock := &fakeFolderService{ - GetFolderByUIDResult: &models.Folder{ - Id: 1, - Uid: "uid", - Title: "Folder", - }, - } - - mockFolderService(mock) + folderResponse := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"} + folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResponse, nil).Once() cmd := dtos.UpdateDashboardAclCommand{ Items: []dtos.DashboardAclUpdateItem{ @@ -249,10 +222,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { t.Run("When trying to override inherited permissions with lower precedence", func(t *testing.T) { origNewGuardian := guardian.New - origNewFolderService := dashboards.NewFolderService t.Cleanup(func() { guardian.New = origNewGuardian - dashboards.NewFolderService = origNewFolderService }) guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ @@ -261,15 +232,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride}, ) - mock := &fakeFolderService{ - GetFolderByUIDResult: &models.Folder{ - Id: 1, - Uid: "uid", - Title: "Folder", - }, - } - - mockFolderService(mock) + folderResponse := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"} + folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResponse, nil).Once() cmd := dtos.UpdateDashboardAclCommand{ Items: []dtos.DashboardAclUpdateItem{ @@ -291,14 +255,12 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { t.Run("Getting and updating folder permissions with hidden users", func(t *testing.T) { origNewGuardian := guardian.New - origNewFolderService := dashboards.NewFolderService settings.HiddenUsers = map[string]struct{}{ "hiddenUser": {}, testUserLogin: {}, } t.Cleanup(func() { guardian.New = origNewGuardian - dashboards.NewFolderService = origNewFolderService settings.HiddenUsers = make(map[string]struct{}) }) @@ -315,15 +277,13 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { }, }) - mock := &fakeFolderService{ - GetFolderByUIDResult: &models.Folder{ - Id: 1, - Uid: "uid", - Title: "Folder", - }, - } + var gotItems []*models.DashboardAcl - mockFolderService(mock) + folderResponse := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"} + folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResponse, nil).Twice() + dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + gotItems = args.Get(2).([]*models.DashboardAcl) + }).Return(nil).Once() var resp []*models.DashboardAclInfoDTO mockSQLStore := mockstore.NewSQLStoreMock() @@ -360,16 +320,6 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { routePattern: "/api/folders/:uid/permissions", cmd: cmd, fn: func(sc *scenarioContext) { - origUpdateDashboardACL := updateDashboardACL - t.Cleanup(func() { - updateDashboardACL = origUpdateDashboardACL - }) - var gotItems []*models.DashboardAcl - updateDashboardACL = func(_ context.Context, _ dashboardifaces.Store, _ int64, items []*models.DashboardAcl) error { - gotItems = items - return nil - } - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) assert.Len(t, gotItems, 4) @@ -385,21 +335,12 @@ func callGetFolderPermissions(sc *scenarioContext, hs *HTTPServer) { func callUpdateFolderPermissions(t *testing.T, sc *scenarioContext) { t.Helper() - - origUpdateDashboardACL := updateDashboardACL - t.Cleanup(func() { - updateDashboardACL = origUpdateDashboardACL - }) - updateDashboardACL = func(_ context.Context, _ dashboardifaces.Store, dashID int64, items []*models.DashboardAcl) error { - return nil - } - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } func updateFolderPermissionScenario(t *testing.T, ctx updatePermissionContext, hs *HTTPServer) { t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) { - defer bus.ClearBusHandlers() + t.Cleanup(bus.ClearBusHandlers) sc := setupScenarioContext(t, ctx.url) diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 08ae3853a3b..cf10dc46952 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -10,26 +10,28 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" - dboards "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) func TestFoldersAPIEndpoint(t *testing.T) { + folderService := &dashboards.FakeFolderService{} + defer folderService.AssertExpectations(t) + t.Run("Given a correct request for creating a folder", func(t *testing.T) { cmd := models.CreateFolderCommand{ Uid: "uid", Title: "Folder", } - mock := &fakeFolderService{ - CreateFolderResult: &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}, - } + folderResult := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"} + folderService.On("CreateFolder", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResult, nil).Once() - createFolderScenario(t, "When calling POST on", "/api/folders", "/api/folders", mock, cmd, + createFolderScenario(t, "When calling POST on", "/api/folders", "/api/folders", folderService, cmd, func(sc *scenarioContext) { callCreateFolder(sc) @@ -64,12 +66,10 @@ func TestFoldersAPIEndpoint(t *testing.T) { } for _, tc := range testCases { - mock := &fakeFolderService{ - CreateFolderError: tc.Error, - } + folderService.On("CreateFolder", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, tc.Error).Once() createFolderScenario(t, fmt.Sprintf("Expect '%s' error when calling POST on", tc.Error.Error()), - "/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) { + "/api/folders", "/api/folders", folderService, cmd, func(sc *scenarioContext) { callCreateFolder(sc) assert.Equalf(t, tc.ExpectedStatusCode, sc.resp.Code, "Wrong status code for error %s", tc.Error) }) @@ -81,11 +81,12 @@ func TestFoldersAPIEndpoint(t *testing.T) { Title: "Folder upd", } - mock := &fakeFolderService{ - UpdateFolderResult: &models.Folder{Id: 1, Uid: "uid", Title: "Folder upd"}, - } + folderService.On("UpdateFolder", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + cmd := args.Get(4).(*models.UpdateFolderCommand) + cmd.Result = &models.Folder{Id: 1, Uid: "uid", Title: "Folder upd"} + }).Return(nil).Once() - updateFolderScenario(t, "When calling PUT on", "/api/folders/uid", "/api/folders/:uid", mock, cmd, + updateFolderScenario(t, "When calling PUT on", "/api/folders/uid", "/api/folders/:uid", folderService, cmd, func(sc *scenarioContext) { callUpdateFolder(sc) @@ -119,12 +120,9 @@ func TestFoldersAPIEndpoint(t *testing.T) { } for _, tc := range testCases { - mock := &fakeFolderService{ - UpdateFolderError: tc.Error, - } - + folderService.On("UpdateFolder", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.Error).Once() updateFolderScenario(t, fmt.Sprintf("Expect '%s' error when calling PUT on", tc.Error.Error()), - "/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) { + "/api/folders/uid", "/api/folders/:uid", folderService, cmd, func(sc *scenarioContext) { callUpdateFolder(sc) assert.Equalf(t, tc.ExpectedStatusCode, sc.resp.Code, "Wrong status code for %s", tc.Error) }) @@ -136,14 +134,15 @@ func callCreateFolder(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } -func createFolderScenario(t *testing.T, desc string, url string, routePattern string, mock *fakeFolderService, +func createFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService, cmd models.CreateFolderCommand, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { t.Cleanup(bus.ClearBusHandlers) hs := HTTPServer{ - Bus: bus.GetBus(), - Cfg: setting.NewCfg(), + Bus: bus.GetBus(), + Cfg: setting.NewCfg(), + folderService: folderService, } sc := setupScenarioContext(t, url) @@ -156,15 +155,8 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st return hs.CreateFolder(c) }) - origNewFolderService := dashboards.NewFolderService - mockFolderService(mock) - sc.m.Post(routePattern, sc.defaultHandler) - defer func() { - dashboards.NewFolderService = origNewFolderService - }() - fn(sc) }) } @@ -173,13 +165,14 @@ func callUpdateFolder(sc *scenarioContext) { sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() } -func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, mock *fakeFolderService, +func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService, cmd models.UpdateFolderCommand, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() hs := HTTPServer{ - Cfg: setting.NewCfg(), + Cfg: setting.NewCfg(), + folderService: folderService, } sc := setupScenarioContext(t, url) @@ -192,12 +185,6 @@ func updateFolderScenario(t *testing.T, desc string, url string, routePattern st return hs.UpdateFolder(c) }) - origNewFolderService := dashboards.NewFolderService - t.Cleanup(func() { - dashboards.NewFolderService = origNewFolderService - }) - mockFolderService(mock) - sc.m.Put(routePattern, sc.defaultHandler) fn(sc) @@ -222,35 +209,28 @@ type fakeFolderService struct { DeletedFolderUids []string } -func (s *fakeFolderService) GetFolders(ctx context.Context, limit int64, page int64) ([]*models.Folder, error) { +func (s *fakeFolderService) GetFolders(ctx context.Context, user *models.SignedInUser, orgID int64, limit int64, page int64) ([]*models.Folder, error) { return s.GetFoldersResult, s.GetFoldersError } -func (s *fakeFolderService) GetFolderByID(ctx context.Context, id int64) (*models.Folder, error) { +func (s *fakeFolderService) GetFolderByID(ctx context.Context, user *models.SignedInUser, id int64, orgID int64) (*models.Folder, error) { return s.GetFolderByIDResult, s.GetFolderByIDError } -func (s *fakeFolderService) GetFolderByUID(ctx context.Context, uid string) (*models.Folder, error) { +func (s *fakeFolderService) GetFolderByUID(ctx context.Context, user *models.SignedInUser, orgID int64, uid string) (*models.Folder, error) { return s.GetFolderByUIDResult, s.GetFolderByUIDError } -func (s *fakeFolderService) CreateFolder(ctx context.Context, title, uid string) (*models.Folder, error) { +func (s *fakeFolderService) CreateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, title, uid string) (*models.Folder, error) { return s.CreateFolderResult, s.CreateFolderError } -func (s *fakeFolderService) UpdateFolder(ctx context.Context, existingUID string, cmd *models.UpdateFolderCommand) error { +func (s *fakeFolderService) UpdateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) error { cmd.Result = s.UpdateFolderResult return s.UpdateFolderError } -func (s *fakeFolderService) DeleteFolder(ctx context.Context, uid string, forceDeleteRules bool) (*models.Folder, error) { +func (s *fakeFolderService) DeleteFolder(ctx context.Context, user *models.SignedInUser, orgID int64, uid string, forceDeleteRules bool) (*models.Folder, error) { s.DeletedFolderUids = append(s.DeletedFolderUids, uid) return s.DeleteFolderResult, s.DeleteFolderError } - -func mockFolderService(mock *fakeFolderService) { - dashboards.NewFolderService = func(orgId int64, user *models.SignedInUser, - dashboardStore dboards.Store) dashboards.FolderService { - return mock - } -} diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index af58dbc2744..15fa35dd508 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -33,6 +33,7 @@ import ( "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/cleanup" "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/encryption" @@ -130,6 +131,9 @@ type HTTPServer struct { authInfoService login.AuthInfoService TeamPermissionsService *resourcepermissions.Service NotificationService *notifications.NotificationService + dashboardService dashboards.DashboardService + dashboardProvisioningService dashboards.DashboardProvisioningService + folderService dashboards.FolderService DatasourcePermissionsService DatasourcePermissionsService } @@ -158,7 +162,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi dataSourcesService datasources.DataSourceService, secretsService secrets.Service, queryDataService *query.Service, ldapGroups ldap.Groups, teamGuardian teamguardian.TeamGuardian, serviceaccountsService serviceaccounts.Service, authInfoService login.AuthInfoService, resourcePermissionServices *resourceservices.ResourceServices, - notificationService *notifications.NotificationService, datasourcePermissionsService DatasourcePermissionsService) (*HTTPServer, error) { + notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService, dashboardProvisioningService dashboards.DashboardProvisioningService, + folderService dashboards.FolderService, datasourcePermissionsService DatasourcePermissionsService) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -219,6 +224,9 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi authInfoService: authInfoService, TeamPermissionsService: resourcePermissionServices.GetTeamService(), NotificationService: notificationService, + dashboardService: dashboardService, + dashboardProvisioningService: dashboardProvisioningService, + folderService: folderService, DatasourcePermissionsService: datasourcePermissionsService, } if hs.Listener != nil { diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index ee5dedb51c7..a098d163932 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -150,7 +150,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { "tags": "prod", }), } - folder, err := sc.db.SaveDashboard(cmd) + folder, err := sc.dashboardsStore.SaveDashboard(cmd) require.NoError(t, err) require.NotNil(t, folder) @@ -165,7 +165,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { Updated: time.Now(), }, } - err = sc.db.UpdateDashboardACL(context.Background(), folder.Id, acls) + err = sc.dashboardsStore.UpdateDashboardACL(context.Background(), folder.Id, acls) require.NoError(t, err) response := callAPI(sc.server, http.MethodGet, "/api/org/users/lookup", nil, t) diff --git a/pkg/dashboards/ifaces.go b/pkg/dashboards/ifaces.go deleted file mode 100644 index 93fe150e583..00000000000 --- a/pkg/dashboards/ifaces.go +++ /dev/null @@ -1,23 +0,0 @@ -package dashboards - -import ( - "context" - - "github.com/grafana/grafana/pkg/models" -) - -// Store is a dashboard store. -type Store interface { - // ValidateDashboardBeforeSave validates a dashboard before save. - ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) - // GetFolderByTitle retrieves a dashboard by its title and is used by unified alerting - GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error) - GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) - GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) - GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) - SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) - SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) - UpdateDashboardACLCtx(ctx context.Context, uid int64, items []*models.DashboardAcl) error - // SaveAlerts saves dashboard alerts. - SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error -} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index ff11aa463b3..358f0ad1e8b 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -456,7 +456,3 @@ type GetDashboardRefByIdQuery struct { Id int64 Result *DashboardRef } - -type UnprovisionDashboardCommand struct { - Id int64 -} diff --git a/pkg/plugins/manager/dashboards_test.go b/pkg/plugins/manager/dashboards_test.go index 55397f0bff2..d801c65e78b 100644 --- a/pkg/plugins/manager/dashboards_test.go +++ b/pkg/plugins/manager/dashboards_test.go @@ -11,6 +11,9 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/services/dashboards/database" + service "github.com/grafana/grafana/pkg/services/dashboards/manager" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" ) @@ -24,8 +27,9 @@ func TestGetPluginDashboards(t *testing.T) { }, } pmCfg := plugins.FromGrafanaCfg(cfg) + dashboardService := service.ProvideDashboardService(database.ProvideDashboardStore(&sqlstore.SQLStore{})) pm, err := ProvideService(cfg, loader.New(pmCfg, nil, - signature.NewUnsignedAuthorizer(pmCfg), &provider.Service{})) + signature.NewUnsignedAuthorizer(pmCfg), &provider.Service{}), dashboardService) require.NoError(t, err) bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardQuery) error { diff --git a/pkg/plugins/manager/manager.go b/pkg/plugins/manager/manager.go index 616f862d97b..d74bad4f744 100644 --- a/pkg/plugins/manager/manager.go +++ b/pkg/plugins/manager/manager.go @@ -9,12 +9,12 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/instrumentation" "github.com/grafana/grafana/pkg/plugins/manager/installer" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -30,35 +30,38 @@ var _ plugins.StaticRouteResolver = (*PluginManager)(nil) var _ plugins.RendererManager = (*PluginManager)(nil) type PluginManager struct { - cfg *plugins.Cfg - store map[string]*plugins.Plugin - pluginInstaller plugins.Installer - pluginLoader plugins.Loader - pluginsMu sync.RWMutex - pluginPaths map[plugins.Class][]string - log log.Logger + cfg *plugins.Cfg + store map[string]*plugins.Plugin + pluginInstaller plugins.Installer + pluginLoader plugins.Loader + pluginsMu sync.RWMutex + pluginPaths map[plugins.Class][]string + dashboardService dashboards.DashboardService + log log.Logger } -func ProvideService(grafanaCfg *setting.Cfg, pluginLoader plugins.Loader) (*PluginManager, error) { +func ProvideService(grafanaCfg *setting.Cfg, pluginLoader plugins.Loader, dashboardService dashboards.DashboardService) (*PluginManager, error) { pm := New(plugins.FromGrafanaCfg(grafanaCfg), map[plugins.Class][]string{ plugins.Core: corePluginPaths(grafanaCfg), plugins.Bundled: {grafanaCfg.BundledPluginsPath}, plugins.External: append([]string{grafanaCfg.PluginsPath}, pluginSettingPaths(grafanaCfg)...), - }, pluginLoader) + }, pluginLoader, dashboardService) if err := pm.Init(); err != nil { return nil, err } return pm, nil } -func New(cfg *plugins.Cfg, pluginPaths map[plugins.Class][]string, pluginLoader plugins.Loader) *PluginManager { +func New(cfg *plugins.Cfg, pluginPaths map[plugins.Class][]string, pluginLoader plugins.Loader, + dashboardService dashboards.DashboardService) *PluginManager { return &PluginManager{ - cfg: cfg, - pluginLoader: pluginLoader, - pluginPaths: pluginPaths, - store: make(map[string]*plugins.Plugin), - log: log.New("plugin.manager"), - pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)), + cfg: cfg, + pluginLoader: pluginLoader, + pluginPaths: pluginPaths, + store: make(map[string]*plugins.Plugin), + log: log.New("plugin.manager"), + pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)), + dashboardService: dashboardService, } } diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index beb81cb7a96..fb4f358f08c 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -7,16 +7,14 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/infra/tracing" - "go.opentelemetry.io/otel/trace" - "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/signature" + service "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/searchV2" @@ -37,6 +35,7 @@ import ( "github.com/grafana/grafana/pkg/tsdb/prometheus" "github.com/grafana/grafana/pkg/tsdb/tempo" "github.com/grafana/grafana/pkg/tsdb/testdatasource" + "go.opentelemetry.io/otel/trace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -95,7 +94,7 @@ func TestPluginManager_int_init(t *testing.T) { pmCfg := plugins.FromGrafanaCfg(cfg) pm, err := ProvideService(cfg, loader.New(pmCfg, license, signature.NewUnsignedAuthorizer(pmCfg), - provider.ProvideService(coreRegistry))) + provider.ProvideService(coreRegistry)), &service.DashboardServiceImpl{}) require.NoError(t, err) verifyCorePluginCatalogue(t, pm) diff --git a/pkg/plugins/manager/manager_test.go b/pkg/plugins/manager/manager_test.go index d0b5d9dc813..6a35bd463d0 100644 --- a/pkg/plugins/manager/manager_test.go +++ b/pkg/plugins/manager/manager_test.go @@ -12,6 +12,9 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" + "github.com/grafana/grafana/pkg/services/dashboards/database" + service "github.com/grafana/grafana/pkg/services/dashboards/manager" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -466,7 +469,8 @@ func TestPluginManager_lifecycle_unmanaged(t *testing.T) { func createManager(t *testing.T, cbs ...func(*PluginManager)) *PluginManager { t.Helper() - pm := New(&plugins.Cfg{}, nil, &fakeLoader{}) + dashboardService := service.ProvideDashboardService(database.ProvideDashboardStore(&sqlstore.SQLStore{})) + pm := New(&plugins.Cfg{}, nil, &fakeLoader{}, dashboardService) for _, cb := range cbs { cb(pm) @@ -520,7 +524,8 @@ func newScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *managerS cfg.Azure.ManagedIdentityClientId = "client-id" loader := &fakeLoader{} - manager := New(cfg, nil, loader) + dashboardService := service.ProvideDashboardService(database.ProvideDashboardStore(&sqlstore.SQLStore{})) + manager := New(cfg, nil, loader, dashboardService) manager.pluginLoader = loader ctx := &managerScenarioCtx{ manager: manager, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 3e6f8dbb1e0..5d1b5cd5ad3 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -34,6 +34,9 @@ import ( "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/dashboardimport" dashboardimportservice "github.com/grafana/grafana/pkg/services/dashboardimport/service" + "github.com/grafana/grafana/pkg/services/dashboards" + dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database" + dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" @@ -200,6 +203,13 @@ var wireBasicSet = wire.NewSet( featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, resourceservices.ProvideResourceServices, + dashboardservice.ProvideDashboardService, + dashboardservice.ProvideFolderService, + dashboardstore.ProvideDashboardStore, + wire.Bind(new(dashboards.DashboardService), new(*dashboardservice.DashboardServiceImpl)), + wire.Bind(new(dashboards.DashboardProvisioningService), new(*dashboardservice.DashboardServiceImpl)), + wire.Bind(new(dashboards.FolderService), new(*dashboardservice.FolderServiceImpl)), + wire.Bind(new(dashboards.Store), new(*dashboardstore.DashboardStore)), dashboardimportservice.ProvideService, wire.Bind(new(dashboardimport.Service), new(*dashboardimportservice.ImportDashboardService)), plugindashboards.ProvideService, diff --git a/pkg/services/dashboardimport/service/service.go b/pkg/services/dashboardimport/service/service.go index 1fcfeb3d836..eead5a7b046 100644 --- a/pkg/services/dashboardimport/service/service.go +++ b/pkg/services/dashboardimport/service/service.go @@ -13,16 +13,15 @@ import ( "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/schemaloader" - "github.com/grafana/grafana/pkg/services/sqlstore" ) -func ProvideService(sqlStore *sqlstore.SQLStore, routeRegister routing.RouteRegister, +func ProvideService(routeRegister routing.RouteRegister, quotaService *quota.QuotaService, schemaLoaderService *schemaloader.SchemaLoaderService, pluginDashboardManager plugins.PluginDashboardManager, pluginStore plugins.Store, - libraryPanelService librarypanels.Service) *ImportDashboardService { + libraryPanelService librarypanels.Service, dashboardService dashboards.DashboardService) *ImportDashboardService { s := &ImportDashboardService{ pluginDashboardManager: pluginDashboardManager, - dashboardService: dashboards.NewService(sqlStore), + dashboardService: dashboardService, libraryPanelService: libraryPanelService, } diff --git a/pkg/services/dashboards/acl_service.go b/pkg/services/dashboards/acl_service.go deleted file mode 100644 index 0a9b7b4f024..00000000000 --- a/pkg/services/dashboards/acl_service.go +++ /dev/null @@ -1,51 +0,0 @@ -package dashboards - -import ( - "context" - "time" - - "github.com/grafana/grafana/pkg/models" -) - -func (dr *dashboardServiceImpl) MakeUserAdmin(ctx context.Context, orgID int64, userID int64, dashboardID int64, setViewAndEditPermissions bool) error { - rtEditor := models.ROLE_EDITOR - rtViewer := models.ROLE_VIEWER - - items := []*models.DashboardAcl{ - { - OrgID: orgID, - DashboardID: dashboardID, - UserID: userID, - Permission: models.PERMISSION_ADMIN, - Created: time.Now(), - Updated: time.Now(), - }, - } - - if setViewAndEditPermissions { - items = append(items, - &models.DashboardAcl{ - OrgID: orgID, - DashboardID: dashboardID, - Role: &rtEditor, - Permission: models.PERMISSION_EDIT, - Created: time.Now(), - Updated: time.Now(), - }, - &models.DashboardAcl{ - OrgID: orgID, - DashboardID: dashboardID, - Role: &rtViewer, - Permission: models.PERMISSION_VIEW, - Created: time.Now(), - Updated: time.Now(), - }, - ) - } - - if err := dr.dashboardStore.UpdateDashboardACLCtx(ctx, dashboardID, items); err != nil { - return err - } - - return nil -} diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go new file mode 100644 index 00000000000..f3cca22bfe4 --- /dev/null +++ b/pkg/services/dashboards/dashboard.go @@ -0,0 +1,47 @@ +package dashboards + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" +) + +// DashboardService is a service for operating on dashboards. +type DashboardService interface { + SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) + ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) + DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error + MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error + BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) + UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error +} + +//go:generate mockery --name DashboardProvisioningService --structname FakeDashboardProvisioning --inpackage --filename dashboard_provisioning_mock.go +// DashboardProvisioningService is a service for operating on provisioned dashboards. +type DashboardProvisioningService interface { + SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) + SaveFolderForProvisionedDashboards(context.Context, *SaveDashboardDTO) (*models.Dashboard, error) + GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) + GetProvisionedDashboardDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) + GetProvisionedDashboardDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) + UnprovisionDashboard(ctx context.Context, dashboardID int64) error + DeleteProvisionedDashboard(ctx context.Context, dashboardID int64, orgID int64) error +} + +//go:generate mockery --name Store --structname FakeDashboardStore --output database --outpkg database --filename database_mock.go +// Store is a dashboard store. +type Store interface { + // ValidateDashboardBeforeSave validates a dashboard before save. + ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) + // GetFolderByTitle retrieves a dashboard by its title and is used by unified alerting + GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error) + GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) + GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) + GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) + 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 + // 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 new file mode 100644 index 00000000000..46ae5a09745 --- /dev/null +++ b/pkg/services/dashboards/dashboard_provisioning_mock.go @@ -0,0 +1,158 @@ +// Code generated by mockery v2.10.0. DO NOT EDIT. + +package dashboards + +import ( + context "context" + + models "github.com/grafana/grafana/pkg/models" + mock "github.com/stretchr/testify/mock" +) + +// FakeDashboardProvisioning is an autogenerated mock type for the DashboardProvisioningService type +type FakeDashboardProvisioning struct { + mock.Mock +} + +// 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) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok { + r0 = rf(ctx, dashboardID, orgID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetProvisionedDashboardData provides a mock function with given fields: name +func (_m *FakeDashboardProvisioning) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { + ret := _m.Called(name) + + var r0 []*models.DashboardProvisioning + if rf, ok := ret.Get(0).(func(string) []*models.DashboardProvisioning); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.DashboardProvisioning) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetProvisionedDashboardDataByDashboardID provides a mock function with given fields: dashboardID +func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) { + ret := _m.Called(dashboardID) + + var r0 *models.DashboardProvisioning + if rf, ok := ret.Get(0).(func(int64) *models.DashboardProvisioning); ok { + r0 = rf(dashboardID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.DashboardProvisioning) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(dashboardID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetProvisionedDashboardDataByDashboardUID provides a mock function with given fields: orgID, dashboardUID +func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) { + ret := _m.Called(orgID, dashboardUID) + + var r0 *models.DashboardProvisioning + if rf, ok := ret.Get(0).(func(int64, string) *models.DashboardProvisioning); ok { + r0 = rf(orgID, dashboardUID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.DashboardProvisioning) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64, string) error); ok { + r1 = rf(orgID, dashboardUID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SaveFolderForProvisionedDashboards provides a mock function with given fields: _a0, _a1 +func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 context.Context, _a1 *SaveDashboardDTO) (*models.Dashboard, error) { + ret := _m.Called(_a0, _a1) + + var r0 *models.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO) *models.Dashboard); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Dashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *SaveDashboardDTO) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SaveProvisionedDashboard provides a mock function with given fields: ctx, dto, provisioning +func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { + ret := _m.Called(ctx, dto, provisioning) + + var r0 *models.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, *models.DashboardProvisioning) *models.Dashboard); ok { + r0 = rf(ctx, dto, provisioning) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Dashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *SaveDashboardDTO, *models.DashboardProvisioning) error); ok { + r1 = rf(ctx, dto, provisioning) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnprovisionDashboard provides a mock function with given fields: ctx, dashboardID +func (_m *FakeDashboardProvisioning) UnprovisionDashboard(ctx context.Context, dashboardID int64) error { + ret := _m.Called(ctx, dashboardID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, dashboardID) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go new file mode 100644 index 00000000000..9d6331879cb --- /dev/null +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -0,0 +1,44 @@ +package dashboards + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" +) + +type FakeDashboardService struct { + DashboardService + + SaveDashboardResult *models.Dashboard + SaveDashboardError error + SavedDashboards []*SaveDashboardDTO + ProvisionedDashData *models.DashboardProvisioning +} + +func (s *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) { + s.SavedDashboards = append(s.SavedDashboards, dto) + + if s.SaveDashboardResult == nil && s.SaveDashboardError == nil { + s.SaveDashboardResult = dto.Dashboard + } + + return s.SaveDashboardResult, s.SaveDashboardError +} + +func (s *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) { + return s.SaveDashboard(ctx, dto, true) +} + +func (s *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error { + for index, dash := range s.SavedDashboards { + if dash.Dashboard.Id == dashboardId && dash.OrgId == orgId { + s.SavedDashboards = append(s.SavedDashboards[:index], s.SavedDashboards[index+1:]...) + break + } + } + return nil +} + +func (s *FakeDashboardService) GetProvisionedDashboardDataByDashboardID(id int64) (*models.DashboardProvisioning, error) { + return s.ProvisionedDashData, nil +} diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go new file mode 100644 index 00000000000..60b0b44d5f1 --- /dev/null +++ b/pkg/services/dashboards/database/database.go @@ -0,0 +1,611 @@ +package database + +import ( + "context" + "fmt" + "time" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/util" +) + +type DashboardStore struct { + sqlStore *sqlstore.SQLStore + log log.Logger +} + +func ProvideDashboardStore(sqlStore *sqlstore.SQLStore) *DashboardStore { + return &DashboardStore{sqlStore: sqlStore, log: log.New("dashboard-store")} +} + +func (d *DashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) { + isParentFolderChanged := false + err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + var err error + isParentFolderChanged, err = getExistingDashboardByIdOrUidForUpdate(sess, dashboard, d.sqlStore.Dialect, overwrite) + if err != nil { + return err + } + + isParentFolderChanged, err = getExistingDashboardByTitleAndFolder(sess, dashboard, d.sqlStore.Dialect, overwrite, + isParentFolderChanged) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return false, err + } + + return isParentFolderChanged, nil +} + +func (d *DashboardStore) GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error) { + if title == "" { + return nil, models.ErrDashboardIdentifierNotSet + } + + // there is a unique constraint on org_id, folder_id, title + // there are no nested folders so the parent folder id is always 0 + dashboard := models.Dashboard{OrgId: orgID, FolderId: 0, Title: title} + err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + has, err := sess.Table(&models.Dashboard{}).Where("is_folder = " + d.sqlStore.Dialect.BooleanStr(true)).Where("folder_id=0").Get(&dashboard) + if err != nil { + return err + } + if !has { + return models.ErrDashboardNotFound + } + dashboard.SetId(dashboard.Id) + dashboard.SetUid(dashboard.Uid) + return nil + }) + return &dashboard, err +} + +func (d *DashboardStore) GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) { + var data models.DashboardProvisioning + err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + _, err := sess.Where("dashboard_id = ?", dashboardID).Get(&data) + return err + }) + + if data.DashboardId == 0 { + return nil, nil + } + return &data, err +} + +func (d *DashboardStore) GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) { + var provisionedDashboard models.DashboardProvisioning + err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + var dashboard models.Dashboard + exists, err := sess.Where("org_id = ? AND uid = ?", orgID, dashboardUID).Get(&dashboard) + if err != nil { + return err + } + if !exists { + return models. + ErrDashboardNotFound + } + exists, err = sess.Where("dashboard_id = ?", dashboard.Id).Get(&provisionedDashboard) + if err != nil { + return err + } + if !exists { + return models.ErrProvisionedDashboardNotFound + } + return nil + }) + return &provisionedDashboard, err +} + +func (d *DashboardStore) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { + var result []*models.DashboardProvisioning + err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + return sess.Where("name = ?", name).Find(&result) + }) + return result, err +} + +func (d *DashboardStore) SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { + err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + if err := saveDashboard(sess, &cmd); err != nil { + return err + } + + if provisioning.Updated == 0 { + provisioning.Updated = cmd.Result.Updated.Unix() + } + + return saveProvisionedData(sess, provisioning, cmd.Result) + }) + + return cmd.Result, err +} + +func (d *DashboardStore) SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) { + err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + return saveDashboard(sess, &cmd) + }) + return cmd.Result, err +} + +func (d *DashboardStore) UpdateDashboardACL(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error { + return d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + // delete existing items + _, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", dashboardID) + if err != nil { + return fmt.Errorf("deleting from dashboard_acl failed: %w", err) + } + + for _, item := range items { + if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { + return models.ErrDashboardAclInfoMissing + } + + if item.DashboardID == 0 { + return models.ErrDashboardPermissionDashboardEmpty + } + + sess.Nullable("user_id", "team_id") + if _, err := sess.Insert(item); err != nil { + return err + } + } + + // Update dashboard HasAcl flag + dashboard := models.Dashboard{HasAcl: true} + _, err = sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard) + return err + }) +} + +func (d *DashboardStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error { + return d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + existingAlerts, err := GetAlertsByDashboardId2(dashID, sess) + if err != nil { + return err + } + + if err := updateAlerts(existingAlerts, alerts, sess, d.log); err != nil { + return err + } + + if err := deleteMissingAlerts(existingAlerts, alerts, sess, d.log); err != nil { + return err + } + + return nil + }) +} + +// UnprovisionDashboard removes row in dashboard_provisioning for the dashboard making it seem as if manually created. +// The dashboard will still have `created_by = -1` to see it was not created by any particular user. +func (d *DashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error { + return d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + _, err := sess.Where("dashboard_id = ?", id).Delete(&models.DashboardProvisioning{}) + return err + }) +} + +func getExistingDashboardByIdOrUidForUpdate(sess *sqlstore.DBSession, dash *models.Dashboard, dialect migrator.Dialect, overwrite bool) (bool, error) { + dashWithIdExists := false + isParentFolderChanged := false + var existingById models.Dashboard + + if dash.Id > 0 { + var err error + dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById) + if err != nil { + return false, fmt.Errorf("SQL query for existing dashboard by ID failed: %w", err) + } + + if !dashWithIdExists { + return false, models.ErrDashboardNotFound + } + + if dash.Uid == "" { + dash.SetUid(existingById.Uid) + } + } + + dashWithUidExists := false + var existingByUid models.Dashboard + + if dash.Uid != "" { + var err error + dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid) + if err != nil { + return false, fmt.Errorf("SQL query for existing dashboard by UID failed: %w", err) + } + } + + if dash.FolderId > 0 { + var existingFolder models.Dashboard + folderExists, err := sess.Where("org_id=? AND id=? AND is_folder=?", dash.OrgId, dash.FolderId, + dialect.BooleanStr(true)).Get(&existingFolder) + if err != nil { + return false, fmt.Errorf("SQL query for folder failed: %w", err) + } + + if !folderExists { + return false, models.ErrDashboardFolderNotFound + } + } + + if !dashWithIdExists && !dashWithUidExists { + return false, nil + } + + if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id { + return false, models.ErrDashboardWithSameUIDExists + } + + existing := existingById + + if !dashWithIdExists && dashWithUidExists { + dash.SetId(existingByUid.Id) + dash.SetUid(existingByUid.Uid) + existing = existingByUid + + if !dash.IsFolder { + isParentFolderChanged = true + } + } + + if (existing.IsFolder && !dash.IsFolder) || + (!existing.IsFolder && dash.IsFolder) { + return isParentFolderChanged, models.ErrDashboardTypeMismatch + } + + if !dash.IsFolder && dash.FolderId != existing.FolderId { + isParentFolderChanged = true + } + + // check for is someone else has written in between + if dash.Version != existing.Version { + if overwrite { + dash.SetVersion(existing.Version) + } else { + return isParentFolderChanged, models.ErrDashboardVersionMismatch + } + } + + // do not allow plugin dashboard updates without overwrite flag + if existing.PluginId != "" && !overwrite { + return isParentFolderChanged, models.UpdatePluginDashboardError{PluginId: existing.PluginId} + } + + return isParentFolderChanged, nil +} + +func getExistingDashboardByTitleAndFolder(sess *sqlstore.DBSession, dash *models.Dashboard, dialect migrator.Dialect, overwrite, + isParentFolderChanged bool) (bool, error) { + var existing models.Dashboard + exists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, + dialect.BooleanStr(true), dash.FolderId).Get(&existing) + if err != nil { + return isParentFolderChanged, fmt.Errorf("SQL query for existing dashboard by org ID or folder ID failed: %w", err) + } + + if exists && dash.Id != existing.Id { + if existing.IsFolder && !dash.IsFolder { + return isParentFolderChanged, models.ErrDashboardWithSameNameAsFolder + } + + if !existing.IsFolder && dash.IsFolder { + return isParentFolderChanged, models.ErrDashboardFolderWithSameNameAsDashboard + } + + if !dash.IsFolder && (dash.FolderId != existing.FolderId || dash.Id == 0) { + isParentFolderChanged = true + } + + if overwrite { + dash.SetId(existing.Id) + dash.SetUid(existing.Uid) + dash.SetVersion(existing.Version) + } else { + return isParentFolderChanged, models.ErrDashboardWithSameNameInFolderExists + } + } + + return isParentFolderChanged, nil +} + +func saveDashboard(sess *sqlstore.DBSession, cmd *models.SaveDashboardCommand) error { + dash := cmd.GetDashboardModel() + + userId := cmd.UserId + + if userId == 0 { + userId = -1 + } + + if dash.Id > 0 { + var existing models.Dashboard + dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing) + if err != nil { + return err + } + if !dashWithIdExists { + return models.ErrDashboardNotFound + } + + // check for is someone else has written in between + if dash.Version != existing.Version { + if cmd.Overwrite { + dash.SetVersion(existing.Version) + } else { + return models.ErrDashboardVersionMismatch + } + } + + // do not allow plugin dashboard updates without overwrite flag + if existing.PluginId != "" && !cmd.Overwrite { + return models.UpdatePluginDashboardError{PluginId: existing.PluginId} + } + } + + if dash.Uid == "" { + uid, err := generateNewDashboardUid(sess, dash.OrgId) + if err != nil { + return err + } + dash.SetUid(uid) + } + + parentVersion := dash.Version + var affectedRows int64 + var err error + + if dash.Id == 0 { + dash.SetVersion(1) + dash.Created = time.Now() + dash.CreatedBy = userId + dash.Updated = time.Now() + dash.UpdatedBy = userId + metrics.MApiDashboardInsert.Inc() + affectedRows, err = sess.Insert(dash) + } else { + dash.SetVersion(dash.Version + 1) + + if !cmd.UpdatedAt.IsZero() { + dash.Updated = cmd.UpdatedAt + } else { + dash.Updated = time.Now() + } + + dash.UpdatedBy = userId + + affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash) + } + + if err != nil { + return err + } + + if affectedRows == 0 { + return models.ErrDashboardNotFound + } + + dashVersion := &models.DashboardVersion{ + DashboardId: dash.Id, + ParentVersion: parentVersion, + RestoredFrom: cmd.RestoredFrom, + Version: dash.Version, + Created: time.Now(), + CreatedBy: dash.UpdatedBy, + Message: cmd.Message, + Data: dash.Data, + } + + // insert version entry + if affectedRows, err = sess.Insert(dashVersion); err != nil { + return err + } else if affectedRows == 0 { + return models.ErrDashboardNotFound + } + + // delete existing tags + _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id) + if err != nil { + return err + } + + // insert new tags + tags := dash.GetTags() + if len(tags) > 0 { + for _, tag := range tags { + if _, err := sess.Insert(&sqlstore.DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil { + return err + } + } + } + + cmd.Result = dash + + return nil +} + +func generateNewDashboardUid(sess *sqlstore.DBSession, orgId int64) (string, error) { + for i := 0; i < 3; i++ { + uid := util.GenerateShortUID() + + exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&models.Dashboard{}) + if err != nil { + return "", err + } + + if !exists { + return uid, nil + } + } + + return "", models.ErrDashboardFailedGenerateUniqueUid +} + +func saveProvisionedData(sess *sqlstore.DBSession, provisioning *models.DashboardProvisioning, dashboard *models.Dashboard) error { + result := &models.DashboardProvisioning{} + + exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, provisioning.Name).Get(result) + if err != nil { + return err + } + + provisioning.Id = result.Id + provisioning.DashboardId = dashboard.Id + + if exist { + _, err = sess.ID(result.Id).Update(provisioning) + } else { + _, err = sess.Insert(provisioning) + } + + return err +} + +func GetAlertsByDashboardId2(dashboardId int64, sess *sqlstore.DBSession) ([]*models.Alert, error) { + alerts := make([]*models.Alert, 0) + err := sess.Where("dashboard_id = ?", dashboardId).Find(&alerts) + + if err != nil { + return []*models.Alert{}, err + } + + return alerts, nil +} + +func updateAlerts(existingAlerts []*models.Alert, alerts []*models.Alert, sess *sqlstore.DBSession, log log.Logger) error { + for _, alert := range alerts { + update := false + var alertToUpdate *models.Alert + + for _, k := range existingAlerts { + if alert.PanelId == k.PanelId { + update = true + alert.Id = k.Id + alertToUpdate = k + break + } + } + + if update { + if alertToUpdate.ContainsUpdates(alert) { + alert.Updated = time.Now() + alert.State = alertToUpdate.State + sess.MustCols("message", "for") + + _, err := sess.ID(alert.Id).Update(alert) + if err != nil { + return err + } + + log.Debug("Alert updated", "name", alert.Name, "id", alert.Id) + } + } else { + alert.Updated = time.Now() + alert.Created = time.Now() + alert.State = models.AlertStateUnknown + alert.NewStateDate = time.Now() + + _, err := sess.Insert(alert) + if err != nil { + return err + } + + log.Debug("Alert inserted", "name", alert.Name, "id", alert.Id) + } + tags := alert.GetTagsFromSettings() + if _, err := sess.Exec("DELETE FROM alert_rule_tag WHERE alert_id = ?", alert.Id); err != nil { + return err + } + if tags != nil { + tags, err := EnsureTagsExist(sess, tags) + if err != nil { + return err + } + for _, tag := range tags { + if _, err := sess.Exec("INSERT INTO alert_rule_tag (alert_id, tag_id) VALUES(?,?)", alert.Id, tag.Id); err != nil { + return err + } + } + } + } + + return nil +} + +func deleteMissingAlerts(alerts []*models.Alert, existingAlerts []*models.Alert, sess *sqlstore.DBSession, log log.Logger) error { + for _, missingAlert := range alerts { + missing := true + + for _, k := range existingAlerts { + if missingAlert.PanelId == k.PanelId { + missing = false + break + } + } + + if missing { + if err := deleteAlertByIdInternal(missingAlert.Id, "Removed from dashboard", sess, log); err != nil { + // No use trying to delete more, since we're in a transaction and it will be + // rolled back on error. + return err + } + } + } + + return nil +} + +func deleteAlertByIdInternal(alertId int64, reason string, sess *sqlstore.DBSession, log log.Logger) error { + log.Debug("Deleting alert", "id", alertId, "reason", reason) + + if _, err := sess.Exec("DELETE FROM alert WHERE id = ?", alertId); err != nil { + return err + } + + if _, err := sess.Exec("DELETE FROM annotation WHERE alert_id = ?", alertId); err != nil { + return err + } + + if _, err := sess.Exec("DELETE FROM alert_notification_state WHERE alert_id = ?", alertId); err != nil { + return err + } + + if _, err := sess.Exec("DELETE FROM alert_rule_tag WHERE alert_id = ?", alertId); err != nil { + return err + } + + return nil +} + +func EnsureTagsExist(sess *sqlstore.DBSession, tags []*models.Tag) ([]*models.Tag, error) { + for _, tag := range tags { + var existingTag models.Tag + + // check if it exists + exists, err := sess.Table("tag").Where("`key`=? AND `value`=?", tag.Key, tag.Value).Get(&existingTag) + if err != nil { + return nil, err + } + if exists { + tag.Id = existingTag.Id + } else { + _, err := sess.Table("tag").Insert(tag) + if err != nil { + return nil, err + } + } + } + + return tags, nil +} diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/dashboards/database/database_dashboard_test.go similarity index 84% rename from pkg/services/sqlstore/dashboard_test.go rename to pkg/services/dashboards/database/database_dashboard_test.go index 7c8745d74ee..a6d5033d56e 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/dashboards/database/database_dashboard_test.go @@ -1,7 +1,7 @@ //go:build integration // +build integration -package sqlstore +package database import ( "context" @@ -14,24 +14,25 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDashboardDataAccess(t *testing.T) { - var sqlStore *SQLStore + var sqlStore *sqlstore.SQLStore var savedFolder, savedDash, savedDash2 *models.Dashboard + var dashboardStore *DashboardStore setup := func() { - sqlStore = InitTestDB(t) - savedFolder = insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod", "webapp") - savedDash = insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.Id, false, "prod", "webapp") - insertTestDashboard(t, sqlStore, "test dash 45", 1, savedFolder.Id, false, "prod") - savedDash2 = insertTestDashboard(t, sqlStore, "test dash 67", 1, 0, false, "prod") + sqlStore = sqlstore.InitTestDB(t) + dashboardStore = ProvideDashboardStore(sqlStore) + savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") + savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.Id, false, "prod", "webapp") + insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.Id, false, "prod") + savedDash2 = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod") insertTestRule(t, sqlStore, savedFolder.OrgId, savedFolder.Uid) } @@ -115,7 +116,7 @@ func TestDashboardDataAccess(t *testing.T) { t.Run("Should be able to delete dashboard", func(t *testing.T) { setup() - dash := insertTestDashboard(t, sqlStore, "delete me", 1, 0, false, "delete this") + dash := insertTestDashboard(t, dashboardStore, "delete me", 1, 0, false, "delete this") err := sqlStore.DeleteDashboard(context.Background(), &models.DeleteDashboardCommand{ Id: dash.Id, @@ -124,29 +125,6 @@ func TestDashboardDataAccess(t *testing.T) { require.NoError(t, err) }) - t.Run("Should retry generation of uid once if it fails.", func(t *testing.T) { - setup() - timesCalled := 0 - generateNewUid = func() string { - timesCalled += 1 - if timesCalled <= 2 { - return savedDash.Uid - } - return util.GenerateShortUID() - } - cmd := models.SaveDashboardCommand{ - OrgId: 1, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "title": "new dash 12334", - "tags": []interface{}{}, - }), - } - _, err := sqlStore.SaveDashboard(cmd) - require.NoError(t, err) - - generateNewUid = util.GenerateShortUID - }) - t.Run("Should be able to create dashboard", func(t *testing.T) { setup() cmd := models.SaveDashboardCommand{ @@ -157,13 +135,14 @@ func TestDashboardDataAccess(t *testing.T) { }), UserId: 100, } - dashboard, err := sqlStore.SaveDashboard(cmd) + dashboard, err := dashboardStore.SaveDashboard(cmd) require.NoError(t, err) require.EqualValues(t, dashboard.CreatedBy, 100) require.False(t, dashboard.Created.IsZero()) require.EqualValues(t, dashboard.UpdatedBy, 100) require.False(t, dashboard.Updated.IsZero()) }) + t.Run("Should be able to update dashboard by id and remove folderId", func(t *testing.T) { setup() cmd := models.SaveDashboardCommand{ @@ -177,7 +156,7 @@ func TestDashboardDataAccess(t *testing.T) { FolderId: 2, UserId: 100, } - dash, err := sqlStore.SaveDashboard(cmd) + dash, err := dashboardStore.SaveDashboard(cmd) require.NoError(t, err) require.EqualValues(t, dash.FolderId, 2) @@ -192,7 +171,7 @@ func TestDashboardDataAccess(t *testing.T) { Overwrite: true, UserId: 100, } - _, err = sqlStore.SaveDashboard(cmd) + _, err = dashboardStore.SaveDashboard(cmd) require.NoError(t, err) query := models.GetDashboardQuery{ @@ -211,7 +190,7 @@ func TestDashboardDataAccess(t *testing.T) { t.Run("Should be able to delete empty folder", func(t *testing.T) { setup() - emptyFolder := insertTestDashboard(t, sqlStore, "2 test dash folder", 1, 0, true, "prod", "webapp") + emptyFolder := insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, true, "prod", "webapp") deleteCmd := &models.DeleteDashboardCommand{Id: emptyFolder.Id} err := sqlStore.DeleteDashboard(context.Background(), deleteCmd) @@ -242,7 +221,7 @@ func TestDashboardDataAccess(t *testing.T) { require.Equal(t, len(query.Result), 0) - sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { + sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { var existingRuleID int64 exists, err := sess.Table("alert_rule").Where("namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", savedFolder.Id).Cols("id").Get(&existingRuleID) require.NoError(t, err) @@ -258,7 +237,6 @@ func TestDashboardDataAccess(t *testing.T) { }) t.Run("Should return error if no dashboard is found for update when dashboard id is greater than zero", func(t *testing.T) { - setup() cmd := models.SaveDashboardCommand{ OrgId: 1, Overwrite: true, @@ -269,12 +247,11 @@ func TestDashboardDataAccess(t *testing.T) { }), } - _, err := sqlStore.SaveDashboard(cmd) + _, err := dashboardStore.SaveDashboard(cmd) require.Equal(t, err, models.ErrDashboardNotFound) }) t.Run("Should not return error if no dashboard is found for update when dashboard id is zero", func(t *testing.T) { - setup() cmd := models.SaveDashboardCommand{ OrgId: 1, Overwrite: true, @@ -284,7 +261,7 @@ func TestDashboardDataAccess(t *testing.T) { "tags": []interface{}{}, }), } - _, err := sqlStore.SaveDashboard(cmd) + _, err := dashboardStore.SaveDashboard(cmd) require.NoError(t, err) }) @@ -405,7 +382,7 @@ func TestDashboardDataAccess(t *testing.T) { t.Run("Should be able to search for starred dashboards", func(t *testing.T) { setup() - starredDash := insertTestDashboard(t, sqlStore, "starred dash", 1, 0, false) + starredDash := insertTestDashboard(t, dashboardStore, "starred dash", 1, 0, false) err := sqlStore.StarDashboard(context.Background(), &models.StarDashboardCommand{ DashboardId: starredDash.Id, UserId: 10, @@ -431,29 +408,32 @@ func TestDashboardDataAccess(t *testing.T) { } func TestDashboardDataAccessGivenPluginWithImportedDashboards(t *testing.T) { - sqlStore := InitTestDB(t) + sqlStore := sqlstore.InitTestDB(t) + dashboardStore := ProvideDashboardStore(sqlStore) pluginId := "test-app" - appFolder := insertTestDashboardForPlugin(t, sqlStore, "app-test", 1, 0, true, pluginId) - insertTestDashboardForPlugin(t, sqlStore, "app-dash1", 1, appFolder.Id, false, pluginId) - insertTestDashboardForPlugin(t, sqlStore, "app-dash2", 1, appFolder.Id, false, pluginId) + appFolder := insertTestDashboardForPlugin(t, dashboardStore, "app-test", 1, 0, true, pluginId) + insertTestDashboardForPlugin(t, dashboardStore, "app-dash1", 1, appFolder.Id, false, pluginId) + insertTestDashboardForPlugin(t, dashboardStore, "app-dash2", 1, appFolder.Id, false, pluginId) query := models.GetDashboardsByPluginIdQuery{ PluginId: pluginId, OrgId: 1, } - err := GetDashboardsByPluginId(context.Background(), &query) + err := sqlstore.GetDashboardsByPluginId(context.Background(), &query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) } func TestDashboard_SortingOptions(t *testing.T) { + sqlStore := sqlstore.InitTestDB(t) + dashboardStore := ProvideDashboardStore(sqlStore) // insertTestDashboard uses GoConvey's assertions. Workaround. t.Run("test with multiple sorting options", func(t *testing.T) { - sqlStore := InitTestDB(t) - dashB := insertTestDashboard(t, sqlStore, "Beta", 1, 0, false) - dashA := insertTestDashboard(t, sqlStore, "Alfa", 1, 0, false) + sqlStore := sqlstore.InitTestDB(t) + dashB := insertTestDashboard(t, dashboardStore, "Beta", 1, 0, false) + dashA := insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, false) assert.NotZero(t, dashA.Id) assert.Less(t, dashB.Id, dashA.Id) q := &search.FindPersistedDashboardsQuery{ @@ -464,35 +444,16 @@ func TestDashboard_SortingOptions(t *testing.T) { searchstore.TitleSorter{Descending: true}, }, } - dashboards, err := sqlStore.findDashboards(context.Background(), q) + dashboards, err := sqlStore.FindDashboards(context.Background(), q) require.NoError(t, err) require.Len(t, dashboards, 2) assert.Equal(t, dashA.Id, dashboards[0].ID) assert.Equal(t, dashB.Id, dashboards[1].ID) }) } -func insertTestDashboard(t *testing.T, sqlStore *SQLStore, title string, orgId int64, - folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { - t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: folderId, - IsFolder: isFolder, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": nil, - "title": title, - "tags": tags, - }), - } - dash, err := sqlStore.SaveDashboard(cmd) - require.NoError(t, err) - require.NotNil(t, dash) - dash.Data.Set("id", dash.Id) - dash.Data.Set("uid", dash.Uid) - return dash -} -func insertTestRule(t *testing.T, sqlStore *SQLStore, foderOrgID int64, folderUID string) { - sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { + +func insertTestRule(t *testing.T, sqlStore *sqlstore.SQLStore, foderOrgID int64, folderUID string) { + sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { type alertQuery struct { RefID string DatasourceUID string @@ -562,26 +523,8 @@ func insertTestRule(t *testing.T, sqlStore *SQLStore, foderOrgID int64, folderUI return err }) } -func insertTestDashboardForPlugin(t *testing.T, sqlStore *SQLStore, title string, orgId int64, - folderId int64, isFolder bool, pluginId string) *models.Dashboard { - t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: folderId, - IsFolder: isFolder, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": nil, - "title": title, - }), - PluginId: pluginId, - } - dash, err := sqlStore.SaveDashboard(cmd) - require.NoError(t, err) - - return dash -} -func createUser(t *testing.T, sqlStore *SQLStore, name string, role string, isAdmin bool) models.User { +func CreateUser(t *testing.T, sqlStore *sqlstore.SQLStore, name string, role string, isAdmin bool) models.User { t.Helper() setting.AutoAssignOrg = true setting.AutoAssignOrgId = 1 @@ -595,3 +538,59 @@ func createUser(t *testing.T, sqlStore *SQLStore, name string, role string, isAd require.Equal(t, models.RoleType(role), q1.Result[0].Role) return *currentUser } + +func insertTestDashboard(t *testing.T, dashboardStore *DashboardStore, title string, orgId int64, + folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { + t.Helper() + cmd := models.SaveDashboardCommand{ + OrgId: orgId, + FolderId: folderId, + IsFolder: isFolder, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": title, + "tags": tags, + }), + } + dash, err := dashboardStore.SaveDashboard(cmd) + require.NoError(t, err) + require.NotNil(t, dash) + dash.Data.Set("id", dash.Id) + dash.Data.Set("uid", dash.Uid) + return dash +} + +func insertTestDashboardForPlugin(t *testing.T, dashboardStore *DashboardStore, title string, orgId int64, + folderId int64, isFolder bool, pluginId string) *models.Dashboard { + t.Helper() + cmd := models.SaveDashboardCommand{ + OrgId: orgId, + FolderId: folderId, + IsFolder: isFolder, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": title, + }), + PluginId: pluginId, + } + + dash, err := dashboardStore.SaveDashboard(cmd) + require.NoError(t, err) + + return dash +} + +func updateDashboardAcl(t *testing.T, dashboardStore *DashboardStore, dashboardID int64, + items ...models.DashboardAcl) error { + t.Helper() + + var itemPtrs []*models.DashboardAcl + for _, it := range items { + item := it + item.Created = time.Now() + item.Updated = time.Now() + itemPtrs = append(itemPtrs, &item) + } + + return dashboardStore.UpdateDashboardACL(context.Background(), dashboardID, itemPtrs) +} diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go similarity index 81% rename from pkg/services/sqlstore/dashboard_folder_test.go rename to pkg/services/dashboards/database/database_folder_test.go index 3b7007594f0..c77ad36e1d4 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -1,32 +1,34 @@ //go:build integration // +build integration -package sqlstore +package database import ( "context" "testing" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/require" ) func TestDashboardFolderDataAccess(t *testing.T) { t.Run("Testing DB", func(t *testing.T) { - var sqlStore *SQLStore + var sqlStore *sqlstore.SQLStore var folder, dashInRoot, childDash *models.Dashboard var currentUser models.User + var dashboardStore *DashboardStore setup := func() { - sqlStore = InitTestDB(t) - folder = insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod", "webapp") - dashInRoot = insertTestDashboard(t, sqlStore, "test dash 67", 1, 0, false, "prod", "webapp") - childDash = insertTestDashboard(t, sqlStore, "test dash 23", 1, folder.Id, false, "prod", "webapp") - insertTestDashboard(t, sqlStore, "test dash 45", 1, folder.Id, false, "prod") - currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) + sqlStore = sqlstore.InitTestDB(t) + dashboardStore = ProvideDashboardStore(sqlStore) + folder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") + dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod", "webapp") + childDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, folder.Id, false, "prod", "webapp") + insertTestDashboard(t, dashboardStore, "test dash 45", 1, folder.Id, false, "prod") + currentUser = CreateUser(t, sqlStore, "viewer", "Viewer", false) } t.Run("Given one dashboard folder with two dashboards and one dashboard in the root folder", func(t *testing.T) { @@ -49,7 +51,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { t.Run("and acl is set for dashboard folder", func(t *testing.T) { var otherUser int64 = 999 - err := testHelperUpdateDashboardAcl(t, sqlStore, folder.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, folder.Id, models.DashboardAcl{ DashboardID: folder.Id, OrgID: 1, UserID: otherUser, @@ -70,7 +72,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) t.Run("when the user is given permission", func(t *testing.T) { - err := testHelperUpdateDashboardAcl(t, sqlStore, folder.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, folder.Id, models.DashboardAcl{ DashboardID: folder.Id, OrgID: 1, UserID: currentUser.Id, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -111,9 +113,9 @@ func TestDashboardFolderDataAccess(t *testing.T) { t.Run("and acl is set for dashboard child and folder has all permissions removed", func(t *testing.T) { var otherUser int64 = 999 - err := testHelperUpdateDashboardAcl(t, sqlStore, folder.Id) + err := updateDashboardAcl(t, dashboardStore, folder.Id) require.NoError(t, err) - err = testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{ + err = updateDashboardAcl(t, dashboardStore, childDash.Id, models.DashboardAcl{ DashboardID: folder.Id, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -129,7 +131,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) t.Run("when the user is given permission to child", func(t *testing.T) { - err := testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, childDash.Id, models.DashboardAcl{ DashboardID: childDash.Id, OrgID: 1, UserID: currentUser.Id, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -167,20 +169,21 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) t.Run("Given two dashboard folders with one dashboard each and one dashboard in the root folder", func(t *testing.T) { - var sqlStore *SQLStore + var sqlStore *sqlstore.SQLStore var folder1, folder2, dashInRoot, childDash1, childDash2 *models.Dashboard var currentUser models.User var rootFolderId int64 = 0 setup2 := func() { - sqlStore = InitTestDB(t) - folder1 = insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod") - folder2 = insertTestDashboard(t, sqlStore, "2 test dash folder", 1, 0, true, "prod") - dashInRoot = insertTestDashboard(t, sqlStore, "test dash 67", 1, 0, false, "prod") - childDash1 = insertTestDashboard(t, sqlStore, "child dash 1", 1, folder1.Id, false, "prod") - childDash2 = insertTestDashboard(t, sqlStore, "child dash 2", 1, folder2.Id, false, "prod") + sqlStore = sqlstore.InitTestDB(t) + dashboardStore := ProvideDashboardStore(sqlStore) + folder1 = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod") + folder2 = insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, true, "prod") + dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod") + childDash1 = insertTestDashboard(t, dashboardStore, "child dash 1", 1, folder1.Id, false, "prod") + childDash2 = insertTestDashboard(t, dashboardStore, "child dash 2", 1, folder2.Id, false, "prod") - currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) + currentUser = CreateUser(t, sqlStore, "viewer", "Viewer", false) } setup2() @@ -205,13 +208,13 @@ func TestDashboardFolderDataAccess(t *testing.T) { t.Run("and acl is set for one dashboard folder", func(t *testing.T) { const otherUser int64 = 999 - err := testHelperUpdateDashboardAcl(t, sqlStore, folder1.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, folder1.Id, models.DashboardAcl{ DashboardID: folder1.Id, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) t.Run("and a dashboard is moved from folder without acl to the folder with an acl", func(t *testing.T) { - moveDashboard(t, sqlStore, 1, childDash2.Data, folder1.Id) + moveDashboard(t, dashboardStore, 1, childDash2.Data, folder1.Id) t.Run("should not return folder with acl or its children", func(t *testing.T) { query := &search.FindPersistedDashboardsQuery{ @@ -227,7 +230,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) t.Run("and a dashboard is moved from folder with acl to the folder without an acl", func(t *testing.T) { setup2() - moveDashboard(t, sqlStore, 1, childDash1.Data, folder2.Id) + moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.Id) t.Run("should return folder without acl and its children", func(t *testing.T) { query := &search.FindPersistedDashboardsQuery{ @@ -246,12 +249,12 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) t.Run("and a dashboard with an acl is moved to the folder without an acl", func(t *testing.T) { - err := testHelperUpdateDashboardAcl(t, sqlStore, childDash1.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, childDash1.Id, models.DashboardAcl{ DashboardID: childDash1.Id, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) - moveDashboard(t, sqlStore, 1, childDash1.Data, folder2.Id) + moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.Id) t.Run("should return folder without acl but not the dashboard with acl", func(t *testing.T) { query := &search.FindPersistedDashboardsQuery{ @@ -272,19 +275,20 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) t.Run("Given two dashboard folders", func(t *testing.T) { - var sqlStore *SQLStore + var sqlStore *sqlstore.SQLStore var folder1, folder2 *models.Dashboard var adminUser, editorUser, viewerUser models.User setup3 := func() { - sqlStore = InitTestDB(t) - folder1 = insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod") - folder2 = insertTestDashboard(t, sqlStore, "2 test dash folder", 1, 0, true, "prod") - insertTestDashboard(t, sqlStore, "folder in another org", 2, 0, true, "prod") + sqlStore = sqlstore.InitTestDB(t) + dashboardStore := ProvideDashboardStore(sqlStore) + folder1 = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod") + folder2 = insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, true, "prod") + insertTestDashboard(t, dashboardStore, "folder in another org", 2, 0, true, "prod") - adminUser = createUser(t, sqlStore, "admin", "Admin", true) - editorUser = createUser(t, sqlStore, "editor", "Editor", false) - viewerUser = createUser(t, sqlStore, "viewer", "Viewer", false) + adminUser = CreateUser(t, sqlStore, "admin", "Admin", true) + editorUser = CreateUser(t, sqlStore, "editor", "Editor", false) + viewerUser = CreateUser(t, sqlStore, "viewer", "Viewer", false) } setup3() @@ -313,7 +317,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { OrgRole: models.ROLE_ADMIN, } - err := GetDashboardPermissionsForUser(context.Background(), &query) + err := sqlstore.GetDashboardPermissionsForUser(context.Background(), &query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) @@ -336,7 +340,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { query := &models.HasAdminPermissionInFoldersQuery{ SignedInUser: &models.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: models.ROLE_ADMIN}, } - err := HasAdminPermissionInFolders(context.Background(), query) + err := sqlstore.HasAdminPermissionInFolders(context.Background(), query) require.NoError(t, err) require.True(t, query.Result) }) @@ -366,7 +370,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { OrgRole: models.ROLE_EDITOR, } - err := GetDashboardPermissionsForUser(context.Background(), &query) + err := sqlstore.GetDashboardPermissionsForUser(context.Background(), &query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) @@ -377,7 +381,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) t.Run("Should have write access to one dashboard folder if default role changed to view for one folder", func(t *testing.T) { - err := testHelperUpdateDashboardAcl(t, sqlStore, folder1.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, folder1.Id, models.DashboardAcl{ DashboardID: folder1.Id, OrgID: 1, UserID: editorUser.Id, Permission: models.PERMISSION_VIEW, }) require.NoError(t, err) @@ -394,7 +398,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { SignedInUser: &models.SignedInUser{UserId: editorUser.Id, OrgId: 1, OrgRole: models.ROLE_EDITOR}, } err := sqlStore.HasEditPermissionInFolders(context.Background(), query) - require.NoError(t, err) + go require.NoError(t, err) require.True(t, query.Result) }) @@ -402,7 +406,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { query := &models.HasAdminPermissionInFoldersQuery{ SignedInUser: &models.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: models.ROLE_EDITOR}, } - err := HasAdminPermissionInFolders(context.Background(), query) + err := sqlstore.HasAdminPermissionInFolders(context.Background(), query) require.NoError(t, err) require.False(t, query.Result) }) @@ -432,7 +436,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { OrgRole: models.ROLE_VIEWER, } - err := GetDashboardPermissionsForUser(context.Background(), &query) + err := sqlstore.GetDashboardPermissionsForUser(context.Background(), &query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) @@ -443,7 +447,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) t.Run("Should be able to get one dashboard folder if default role changed to edit for one folder", func(t *testing.T) { - err := testHelperUpdateDashboardAcl(t, sqlStore, folder1.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, folder1.Id, models.DashboardAcl{ DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.Id, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -462,7 +466,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { SignedInUser: &models.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER}, } err := sqlStore.HasEditPermissionInFolders(context.Background(), query) - require.NoError(t, err) + go require.NoError(t, err) require.False(t, query.Result) }) @@ -470,13 +474,13 @@ func TestDashboardFolderDataAccess(t *testing.T) { query := &models.HasAdminPermissionInFoldersQuery{ SignedInUser: &models.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER}, } - err := HasAdminPermissionInFolders(context.Background(), query) + err := sqlstore.HasAdminPermissionInFolders(context.Background(), query) require.NoError(t, err) require.False(t, query.Result) }) t.Run("and admin permission is given for user with org role viewer in one dashboard folder", func(t *testing.T) { - err := testHelperUpdateDashboardAcl(t, sqlStore, folder1.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, folder1.Id, models.DashboardAcl{ DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.Id, Permission: models.PERMISSION_ADMIN, }) require.NoError(t, err) @@ -486,13 +490,13 @@ func TestDashboardFolderDataAccess(t *testing.T) { SignedInUser: &models.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER}, } err := sqlStore.HasEditPermissionInFolders(context.Background(), query) - require.NoError(t, err) + go require.NoError(t, err) require.True(t, query.Result) }) }) t.Run("and edit permission is given for user with org role viewer in one dashboard folder", func(t *testing.T) { - err := testHelperUpdateDashboardAcl(t, sqlStore, folder1.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, folder1.Id, models.DashboardAcl{ DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.Id, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -502,7 +506,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { SignedInUser: &models.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER}, } err := sqlStore.HasEditPermissionInFolders(context.Background(), query) - require.NoError(t, err) + go require.NoError(t, err) require.True(t, query.Result) }) }) @@ -512,15 +516,16 @@ func TestDashboardFolderDataAccess(t *testing.T) { t.Run("Given dashboard and folder with the same title", func(t *testing.T) { var orgId int64 = 1 title := "Very Unique Name" - var sqlStore *SQLStore + var sqlStore *sqlstore.SQLStore var folder1, folder2 *models.Dashboard - sqlStore = InitTestDB(t) - folder2 = insertTestDashboard(t, sqlStore, "TEST", orgId, 0, true, "prod") - _ = insertTestDashboard(t, sqlStore, title, orgId, folder2.Id, false, "prod") - folder1 = insertTestDashboard(t, sqlStore, title, orgId, 0, true, "prod") + sqlStore = sqlstore.InitTestDB(t) + dashboardStore := ProvideDashboardStore(sqlStore) + folder2 = insertTestDashboard(t, dashboardStore, "TEST", orgId, 0, true, "prod") + _ = insertTestDashboard(t, dashboardStore, title, orgId, folder2.Id, false, "prod") + folder1 = insertTestDashboard(t, dashboardStore, title, orgId, 0, true, "prod") t.Run("GetFolderByTitle should find the folder", func(t *testing.T) { - result, err := sqlStore.GetFolderByTitle(orgId, title) + result, err := dashboardStore.GetFolderByTitle(orgId, title) require.NoError(t, err) require.Equal(t, folder1.Id, result.Id) }) @@ -528,7 +533,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) } -func moveDashboard(t *testing.T, sqlStore *SQLStore, orgId int64, dashboard *simplejson.Json, +func moveDashboard(t *testing.T, dashboardStore *DashboardStore, orgId int64, dashboard *simplejson.Json, newFolderId int64) *models.Dashboard { t.Helper() @@ -538,7 +543,7 @@ func moveDashboard(t *testing.T, sqlStore *SQLStore, orgId int64, dashboard *sim Dashboard: dashboard, Overwrite: true, } - dash, err := sqlStore.SaveDashboard(cmd) + dash, err := dashboardStore.SaveDashboard(cmd) require.NoError(t, err) return dash diff --git a/pkg/services/dashboards/database/database_mock.go b/pkg/services/dashboards/database/database_mock.go new file mode 100644 index 00000000000..c8b154f970f --- /dev/null +++ b/pkg/services/dashboards/database/database_mock.go @@ -0,0 +1,217 @@ +// Code generated by mockery v2.10.0. DO NOT EDIT. + +package database + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + models "github.com/grafana/grafana/pkg/models" +) + +// FakeDashboardStore is an autogenerated mock type for the Store type +type FakeDashboardStore struct { + mock.Mock +} + +// 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) + + var r0 *models.Dashboard + if rf, ok := ret.Get(0).(func(int64, string) *models.Dashboard); ok { + r0 = rf(orgID, title) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Dashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64, string) error); ok { + r1 = rf(orgID, title) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetProvisionedDashboardData provides a mock function with given fields: name +func (_m *FakeDashboardStore) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { + ret := _m.Called(name) + + var r0 []*models.DashboardProvisioning + if rf, ok := ret.Get(0).(func(string) []*models.DashboardProvisioning); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.DashboardProvisioning) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetProvisionedDataByDashboardID provides a mock function with given fields: dashboardID +func (_m *FakeDashboardStore) GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) { + ret := _m.Called(dashboardID) + + var r0 *models.DashboardProvisioning + if rf, ok := ret.Get(0).(func(int64) *models.DashboardProvisioning); ok { + r0 = rf(dashboardID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.DashboardProvisioning) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(dashboardID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetProvisionedDataByDashboardUID provides a mock function with given fields: orgID, dashboardUID +func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) { + ret := _m.Called(orgID, dashboardUID) + + var r0 *models.DashboardProvisioning + if rf, ok := ret.Get(0).(func(int64, string) *models.DashboardProvisioning); ok { + r0 = rf(orgID, dashboardUID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.DashboardProvisioning) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64, string) error); ok { + r1 = rf(orgID, dashboardUID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SaveAlerts provides a mock function with given fields: ctx, dashID, alerts +func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error { + ret := _m.Called(ctx, dashID, alerts) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, []*models.Alert) error); ok { + r0 = rf(ctx, dashID, alerts) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SaveDashboard provides a mock function with given fields: cmd +func (_m *FakeDashboardStore) SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) { + ret := _m.Called(cmd) + + var r0 *models.Dashboard + if rf, ok := ret.Get(0).(func(models.SaveDashboardCommand) *models.Dashboard); ok { + r0 = rf(cmd) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Dashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(models.SaveDashboardCommand) error); ok { + r1 = rf(cmd) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SaveProvisionedDashboard provides a mock function with given fields: cmd, provisioning +func (_m *FakeDashboardStore) SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { + ret := _m.Called(cmd, provisioning) + + var r0 *models.Dashboard + if rf, ok := ret.Get(0).(func(models.SaveDashboardCommand, *models.DashboardProvisioning) *models.Dashboard); ok { + r0 = rf(cmd, provisioning) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Dashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(models.SaveDashboardCommand, *models.DashboardProvisioning) error); ok { + r1 = rf(cmd, provisioning) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnprovisionDashboard provides a mock function with given fields: ctx, id +func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateDashboardACL provides a mock function with given fields: ctx, uid, items +func (_m *FakeDashboardStore) UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error { + ret := _m.Called(ctx, uid, items) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, []*models.DashboardAcl) error); ok { + r0 = rf(ctx, uid, items) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ValidateDashboardBeforeSave provides a mock function with given fields: dashboard, overwrite +func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) { + ret := _m.Called(dashboard, overwrite) + + var r0 bool + if rf, ok := ret.Get(0).(func(*models.Dashboard, bool) bool); ok { + r0 = rf(dashboard, overwrite) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.Dashboard, bool) error); ok { + r1 = rf(dashboard, overwrite) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/dashboards/database/database_provisioning_test.go similarity index 79% rename from pkg/services/sqlstore/dashboard_provisioning_test.go rename to pkg/services/dashboards/database/database_provisioning_test.go index f74800d8851..bec37866108 100644 --- a/pkg/services/sqlstore/dashboard_provisioning_test.go +++ b/pkg/services/dashboards/database/database_provisioning_test.go @@ -1,10 +1,11 @@ //go:build integration // +build integration -package sqlstore +package database import ( "context" + "github.com/grafana/grafana/pkg/services/sqlstore" "testing" "time" @@ -15,7 +16,8 @@ import ( ) func TestDashboardProvisioningTest(t *testing.T) { - sqlStore := InitTestDB(t) + sqlStore := sqlstore.InitTestDB(t) + dashboardStore := ProvideDashboardStore(sqlStore) folderCmd := models.SaveDashboardCommand{ OrgId: 1, @@ -27,7 +29,7 @@ func TestDashboardProvisioningTest(t *testing.T) { }), } - dash, err := sqlStore.SaveDashboard(folderCmd) + dash, err := dashboardStore.SaveDashboard(folderCmd) require.Nil(t, err) saveDashboardCmd := models.SaveDashboardCommand{ @@ -49,7 +51,7 @@ func TestDashboardProvisioningTest(t *testing.T) { Updated: now.Unix(), } - dash, err := sqlStore.SaveProvisionedDashboard(saveDashboardCmd, provisioning) + dash, err := dashboardStore.SaveProvisionedDashboard(saveDashboardCmd, provisioning) require.Nil(t, err) require.NotNil(t, dash) require.NotEqual(t, 0, dash.Id) @@ -71,7 +73,7 @@ func TestDashboardProvisioningTest(t *testing.T) { Updated: now.Unix(), } - anotherDash, err := sqlStore.SaveProvisionedDashboard(saveCmd, provisioning) + anotherDash, err := dashboardStore.SaveProvisionedDashboard(saveCmd, provisioning) require.Nil(t, err) query := &models.GetDashboardsQuery{DashboardIds: []int64{anotherDash.Id}} @@ -91,7 +93,7 @@ func TestDashboardProvisioningTest(t *testing.T) { }) t.Run("Can query for provisioned dashboards", func(t *testing.T) { - rslt, err := sqlStore.GetProvisionedDashboardData("default") + rslt, err := dashboardStore.GetProvisionedDashboardData("default") require.Nil(t, err) require.Equal(t, 1, len(rslt)) @@ -100,13 +102,13 @@ func TestDashboardProvisioningTest(t *testing.T) { }) t.Run("Can query for one provisioned dashboard", func(t *testing.T) { - data, err := sqlStore.GetProvisionedDataByDashboardID(dash.Id) + data, err := dashboardStore.GetProvisionedDataByDashboardID(dash.Id) require.Nil(t, err) require.NotNil(t, data) }) t.Run("Can query for none provisioned dashboard", func(t *testing.T) { - data, err := sqlStore.GetProvisionedDataByDashboardID(3000) + data, err := dashboardStore.GetProvisionedDataByDashboardID(3000) require.Nil(t, err) require.Nil(t, data) }) @@ -119,19 +121,15 @@ func TestDashboardProvisioningTest(t *testing.T) { require.Nil(t, sqlStore.DeleteDashboard(context.Background(), deleteCmd)) - data, err := sqlStore.GetProvisionedDataByDashboardID(dash.Id) + data, err := dashboardStore.GetProvisionedDataByDashboardID(dash.Id) require.Nil(t, err) require.Nil(t, data) }) t.Run("UnprovisionDashboard should delete provisioning metadata", func(t *testing.T) { - unprovisionCmd := &models.UnprovisionDashboardCommand{ - Id: dashId, - } + require.Nil(t, dashboardStore.UnprovisionDashboard(context.Background(), dashId)) - require.Nil(t, UnprovisionDashboard(context.Background(), unprovisionCmd)) - - data, err := sqlStore.GetProvisionedDataByDashboardID(dashId) + data, err := dashboardStore.GetProvisionedDataByDashboardID(dashId) require.Nil(t, err) require.Nil(t, data) }) diff --git a/pkg/services/sqlstore/dashboard_acl_test.go b/pkg/services/dashboards/database/permissions/database_acl_test.go similarity index 72% rename from pkg/services/sqlstore/dashboard_acl_test.go rename to pkg/services/dashboards/database/permissions/database_acl_test.go index 2a29fade832..f0d74fc4719 100644 --- a/pkg/services/sqlstore/dashboard_acl_test.go +++ b/pkg/services/dashboards/database/permissions/database_acl_test.go @@ -1,11 +1,16 @@ //go:build integration // +build integration -package sqlstore +package permissions import ( "context" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/dashboards/database" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" "testing" + "time" "github.com/grafana/grafana/pkg/models" @@ -13,20 +18,22 @@ import ( ) func TestDashboardAclDataAccess(t *testing.T) { - var sqlStore *SQLStore + var sqlStore *sqlstore.SQLStore var currentUser models.User var savedFolder, childDash *models.Dashboard + var dashboardStore *database.DashboardStore setup := func(t *testing.T) { - sqlStore = InitTestDB(t) + sqlStore = sqlstore.InitTestDB(t) + dashboardStore = database.ProvideDashboardStore(sqlStore) currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) - savedFolder = insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod", "webapp") - childDash = insertTestDashboard(t, sqlStore, "2 test dash", 1, savedFolder.Id, false, "prod", "webapp") + savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") + childDash = insertTestDashboard(t, dashboardStore, "2 test dash", 1, savedFolder.Id, false, "prod", "webapp") } t.Run("Dashboard permission with userId and teamId set to 0", func(t *testing.T) { setup(t) - err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, savedFolder.Id, models.DashboardAcl{ OrgID: 1, DashboardID: savedFolder.Id, Permission: models.PERMISSION_EDIT, @@ -70,7 +77,7 @@ func TestDashboardAclDataAccess(t *testing.T) { t.Run("Folder with removed default permissions returns no acl items", func(t *testing.T) { setup(t) - err := sqlStore.UpdateDashboardACL(context.Background(), savedFolder.Id, nil) + err := dashboardStore.UpdateDashboardACL(context.Background(), savedFolder.Id, nil) require.Nil(t, err) query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1} @@ -84,7 +91,7 @@ func TestDashboardAclDataAccess(t *testing.T) { t.Run("Given dashboard folder permission", func(t *testing.T) { setup(t) - err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, savedFolder.Id, models.DashboardAcl{ OrgID: 1, UserID: currentUser.Id, DashboardID: savedFolder.Id, @@ -103,7 +110,7 @@ func TestDashboardAclDataAccess(t *testing.T) { }) t.Run("Given child dashboard permission", func(t *testing.T) { - err := testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, childDash.Id, models.DashboardAcl{ OrgID: 1, UserID: currentUser.Id, DashboardID: childDash.Id, @@ -128,7 +135,7 @@ func TestDashboardAclDataAccess(t *testing.T) { t.Run("Reading dashboard acl should include default acl for parent folder and the child acl", func(t *testing.T) { setup(t) - err := testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, childDash.Id, models.DashboardAcl{ OrgID: 1, UserID: currentUser.Id, DashboardID: childDash.Id, @@ -155,7 +162,7 @@ func TestDashboardAclDataAccess(t *testing.T) { t.Run("Add and delete dashboard permission", func(t *testing.T) { setup(t) - err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ + err := updateDashboardAcl(t, dashboardStore, savedFolder.Id, models.DashboardAcl{ OrgID: 1, UserID: currentUser.Id, DashboardID: savedFolder.Id, @@ -174,7 +181,7 @@ func TestDashboardAclDataAccess(t *testing.T) { require.Equal(t, currentUser.Login, q1.Result[0].UserLogin) require.Equal(t, currentUser.Email, q1.Result[0].UserEmail) - err = testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id) + err = updateDashboardAcl(t, dashboardStore, savedFolder.Id) require.Nil(t, err) q3 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} @@ -188,7 +195,7 @@ func TestDashboardAclDataAccess(t *testing.T) { team1, err := sqlStore.CreateTeam("group1 name", "", 1) require.Nil(t, err) - err = testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ + err = updateDashboardAcl(t, dashboardStore, savedFolder.Id, models.DashboardAcl{ OrgID: 1, TeamID: team1.Id, DashboardID: savedFolder.Id, @@ -208,7 +215,7 @@ func TestDashboardAclDataAccess(t *testing.T) { setup(t) team1, err := sqlStore.CreateTeam("group1 name", "", 1) require.Nil(t, err) - err = testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ + err = updateDashboardAcl(t, dashboardStore, savedFolder.Id, models.DashboardAcl{ OrgID: 1, TeamID: team1.Id, DashboardID: savedFolder.Id, @@ -229,7 +236,7 @@ func TestDashboardAclDataAccess(t *testing.T) { t.Run("Default permissions for root folder dashboards", func(t *testing.T) { setup(t) var rootFolderId int64 = 0 - sqlStore := InitTestDB(t) + sqlStore := sqlstore.InitTestDB(t) query := models.GetDashboardAclInfoListQuery{DashboardID: rootFolderId, OrgID: 1} @@ -246,3 +253,54 @@ func TestDashboardAclDataAccess(t *testing.T) { require.False(t, query.Result[1].Inherited) }) } + +func createUser(t *testing.T, sqlStore *sqlstore.SQLStore, name string, role string, isAdmin bool) models.User { + t.Helper() + setting.AutoAssignOrg = true + setting.AutoAssignOrgId = 1 + setting.AutoAssignOrgRole = role + currentUserCmd := models.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin} + currentUser, err := sqlStore.CreateUser(context.Background(), currentUserCmd) + require.NoError(t, err) + q1 := models.GetUserOrgListQuery{UserId: currentUser.Id} + err = sqlStore.GetUserOrgList(context.Background(), &q1) + require.NoError(t, err) + require.Equal(t, models.RoleType(role), q1.Result[0].Role) + return *currentUser +} + +func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore, title string, orgId int64, + folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { + t.Helper() + cmd := models.SaveDashboardCommand{ + OrgId: orgId, + FolderId: folderId, + IsFolder: isFolder, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": title, + "tags": tags, + }), + } + dash, err := dashboardStore.SaveDashboard(cmd) + require.NoError(t, err) + require.NotNil(t, dash) + dash.Data.Set("id", dash.Id) + dash.Data.Set("uid", dash.Uid) + return dash +} + +func updateDashboardAcl(t *testing.T, dashboardStore *database.DashboardStore, dashboardID int64, + items ...models.DashboardAcl) error { + t.Helper() + + var itemPtrs []*models.DashboardAcl + for _, it := range items { + item := it + item.Created = time.Now() + item.Updated = time.Now() + itemPtrs = append(itemPtrs, &item) + } + + return dashboardStore.UpdateDashboardACL(context.Background(), dashboardID, itemPtrs) +} diff --git a/pkg/services/dashboards/folder.go b/pkg/services/dashboards/folder.go new file mode 100644 index 00000000000..42660db3f76 --- /dev/null +++ b/pkg/services/dashboards/folder.go @@ -0,0 +1,20 @@ +package dashboards + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" +) + +//go:generate mockery --name FolderService --structname FakeFolderService --inpackage --filename folder_service_mock.go +// FolderService is a service for operating on folders. +type FolderService interface { + GetFolders(ctx context.Context, user *models.SignedInUser, orgID int64, limit int64, page int64) ([]*models.Folder, error) + GetFolderByID(ctx context.Context, user *models.SignedInUser, id int64, orgID int64) (*models.Folder, error) + GetFolderByUID(ctx context.Context, user *models.SignedInUser, orgID int64, uid string) (*models.Folder, error) + GetFolderByTitle(ctx context.Context, user *models.SignedInUser, orgID int64, title string) (*models.Folder, error) + CreateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, title, uid string) (*models.Folder, error) + UpdateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) error + DeleteFolder(ctx context.Context, user *models.SignedInUser, orgID int64, uid string, forceDeleteRules bool) (*models.Folder, error) + MakeUserAdmin(ctx context.Context, orgID int64, userID, folderID int64, setViewAndEditPermissions bool) error +} diff --git a/pkg/services/dashboards/folder_service_mock.go b/pkg/services/dashboards/folder_service_mock.go new file mode 100644 index 00000000000..3fcc72601b2 --- /dev/null +++ b/pkg/services/dashboards/folder_service_mock.go @@ -0,0 +1,181 @@ +// Code generated by mockery v2.10.0. DO NOT EDIT. + +package dashboards + +import ( + context "context" + + models "github.com/grafana/grafana/pkg/models" + mock "github.com/stretchr/testify/mock" +) + +// FakeFolderService is an autogenerated mock type for the FolderService type +type FakeFolderService struct { + mock.Mock +} + +// CreateFolder provides a mock function with given fields: ctx, user, orgID, title, uid +func (_m *FakeFolderService) CreateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, title string, uid string) (*models.Folder, error) { + ret := _m.Called(ctx, user, orgID, title, uid) + + var r0 *models.Folder + if rf, ok := ret.Get(0).(func(context.Context, *models.SignedInUser, int64, string, string) *models.Folder); ok { + r0 = rf(ctx, user, orgID, title, uid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Folder) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.SignedInUser, int64, string, string) error); ok { + r1 = rf(ctx, user, orgID, title, uid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteFolder provides a mock function with given fields: ctx, user, orgID, uid, forceDeleteRules +func (_m *FakeFolderService) DeleteFolder(ctx context.Context, user *models.SignedInUser, orgID int64, uid string, forceDeleteRules bool) (*models.Folder, error) { + ret := _m.Called(ctx, user, orgID, uid, forceDeleteRules) + + var r0 *models.Folder + if rf, ok := ret.Get(0).(func(context.Context, *models.SignedInUser, int64, string, bool) *models.Folder); ok { + r0 = rf(ctx, user, orgID, uid, forceDeleteRules) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Folder) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.SignedInUser, int64, string, bool) error); ok { + r1 = rf(ctx, user, orgID, uid, forceDeleteRules) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetFolderByID provides a mock function with given fields: ctx, user, id, orgID +func (_m *FakeFolderService) GetFolderByID(ctx context.Context, user *models.SignedInUser, id int64, orgID int64) (*models.Folder, error) { + ret := _m.Called(ctx, user, id, orgID) + + var r0 *models.Folder + if rf, ok := ret.Get(0).(func(context.Context, *models.SignedInUser, int64, int64) *models.Folder); ok { + r0 = rf(ctx, user, id, orgID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Folder) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.SignedInUser, int64, int64) error); ok { + r1 = rf(ctx, user, id, orgID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetFolderByTitle provides a mock function with given fields: ctx, user, orgID, title +func (_m *FakeFolderService) GetFolderByTitle(ctx context.Context, user *models.SignedInUser, orgID int64, title string) (*models.Folder, error) { + ret := _m.Called(ctx, user, orgID, title) + + var r0 *models.Folder + if rf, ok := ret.Get(0).(func(context.Context, *models.SignedInUser, int64, string) *models.Folder); ok { + r0 = rf(ctx, user, orgID, title) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Folder) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.SignedInUser, int64, string) error); ok { + r1 = rf(ctx, user, orgID, title) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetFolderByUID provides a mock function with given fields: ctx, user, orgID, uid +func (_m *FakeFolderService) GetFolderByUID(ctx context.Context, user *models.SignedInUser, orgID int64, uid string) (*models.Folder, error) { + ret := _m.Called(ctx, user, orgID, uid) + + var r0 *models.Folder + if rf, ok := ret.Get(0).(func(context.Context, *models.SignedInUser, int64, string) *models.Folder); ok { + r0 = rf(ctx, user, orgID, uid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Folder) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.SignedInUser, int64, string) error); ok { + r1 = rf(ctx, user, orgID, uid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetFolders provides a mock function with given fields: ctx, user, orgID, limit, page +func (_m *FakeFolderService) GetFolders(ctx context.Context, user *models.SignedInUser, orgID int64, limit int64, page int64) ([]*models.Folder, error) { + ret := _m.Called(ctx, user, orgID, limit, page) + + var r0 []*models.Folder + if rf, ok := ret.Get(0).(func(context.Context, *models.SignedInUser, int64, int64, int64) []*models.Folder); ok { + r0 = rf(ctx, user, orgID, limit, page) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.Folder) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.SignedInUser, int64, int64, int64) error); ok { + r1 = rf(ctx, user, orgID, limit, page) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MakeUserAdmin provides a mock function with given fields: ctx, orgID, userID, folderID, setViewAndEditPermissions +func (_m *FakeFolderService) MakeUserAdmin(ctx context.Context, orgID int64, userID int64, folderID int64, setViewAndEditPermissions bool) error { + ret := _m.Called(ctx, orgID, userID, folderID, setViewAndEditPermissions) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, int64, bool) error); ok { + r0 = rf(ctx, orgID, userID, folderID, setViewAndEditPermissions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateFolder provides a mock function with given fields: ctx, user, orgID, existingUid, cmd +func (_m *FakeFolderService) UpdateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) error { + ret := _m.Called(ctx, user, orgID, existingUid, cmd) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.SignedInUser, int64, string, *models.UpdateFolderCommand) error); ok { + r0 = rf(ctx, user, orgID, existingUid, cmd) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/manager/dashboard_service.go similarity index 60% rename from pkg/services/dashboards/dashboard_service.go rename to pkg/services/dashboards/manager/dashboard_service.go index 1c0035d8737..759582dfcdd 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/manager/dashboard_service.go @@ -1,4 +1,4 @@ -package dashboards +package service import ( "context" @@ -7,86 +7,42 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" - "github.com/grafana/grafana/pkg/dashboards" - "github.com/grafana/grafana/pkg/services/alerting" - "github.com/grafana/grafana/pkg/setting" - "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/alerting" + m "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" ) -// DashboardService is a service for operating on dashboards. -type DashboardService interface { - SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) - ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) - DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error - MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error +type DashboardServiceImpl struct { + dashboardStore m.Store + log log.Logger } -// DashboardProvisioningService is a service for operating on provisioned dashboards. -type DashboardProvisioningService interface { - SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) - SaveFolderForProvisionedDashboards(context.Context, *SaveDashboardDTO) (*models.Dashboard, error) - GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) - GetProvisionedDashboardDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) - GetProvisionedDashboardDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) - UnprovisionDashboard(ctx context.Context, dashboardID int64) error - DeleteProvisionedDashboard(ctx context.Context, dashboardID int64, orgID int64) error -} - -// NewService is a factory for creating a new dashboard service. -var NewService = func(store dashboards.Store) DashboardService { - return &dashboardServiceImpl{ +func ProvideDashboardService(store m.Store) *DashboardServiceImpl { + return &DashboardServiceImpl{ dashboardStore: store, log: log.New("dashboard-service"), } } -// NewProvisioningService is a factory for creating a new dashboard provisioning service. -var NewProvisioningService = func(store dashboards.Store) DashboardProvisioningService { - return NewService(store).(*dashboardServiceImpl) -} - -type SaveDashboardDTO struct { - OrgId int64 - UpdatedAt time.Time - User *models.SignedInUser - Message string - Overwrite bool - Dashboard *models.Dashboard -} - -type dashboardServiceImpl struct { - dashboardStore dashboards.Store - orgId int64 - user *models.SignedInUser - log log.Logger -} - -func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { +func (dr *DashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { return dr.dashboardStore.GetProvisionedDashboardData(name) } -// GetProvisionedData gets provisioned dashboard data. -// -// Stubbable by tests. -var GetProvisionedData = func(store dashboards.Store, dashboardID int64) (*models.DashboardProvisioning, error) { - return store.GetProvisionedDataByDashboardID(dashboardID) +func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) { + return dr.dashboardStore.GetProvisionedDataByDashboardID(dashboardID) } -func (dr *dashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) { - return GetProvisionedData(dr.dashboardStore, dashboardID) -} - -func (dr *dashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) { +func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) { return dr.dashboardStore.GetProvisionedDataByDashboardUID(orgID, dashboardUID) } -func (dr *dashboardServiceImpl) buildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, +func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, dto *m.SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) { dash := dto.Dashboard @@ -175,6 +131,10 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(ctx context.Context, d return cmd, nil } +func (dr *DashboardServiceImpl) UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error { + return dr.dashboardStore.UpdateDashboardACL(ctx, uid, items) +} + var validateAlerts = func(ctx context.Context, dash *models.Dashboard, user *models.SignedInUser) error { extractor := alerting.NewDashAlertExtractor(dash, dash.OrgId, user) return extractor.ValidateAlerts(ctx) @@ -210,7 +170,7 @@ func validateDashboardRefreshInterval(dash *models.Dashboard) error { // UpdateAlerting updates alerting. // // Stubbable by tests. -var UpdateAlerting = func(ctx context.Context, store dashboards.Store, orgID int64, dashboard *models.Dashboard, user *models.SignedInUser) error { +var UpdateAlerting = func(ctx context.Context, store m.Store, orgID int64, dashboard *models.Dashboard, user *models.SignedInUser) error { extractor := alerting.NewDashAlertExtractor(dashboard, orgID, user) alerts, err := extractor.GetAlerts(ctx) if err != nil { @@ -220,7 +180,7 @@ var UpdateAlerting = func(ctx context.Context, store dashboards.Store, orgID int return store.SaveAlerts(ctx, dashboard.Id, alerts) } -func (dr *dashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, +func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dto *m.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for provisioned dashboard to minimum refresh interval", "dashboardUid", @@ -234,7 +194,7 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt OrgId: dto.OrgId, } - cmd, err := dr.buildSaveDashboardCommand(ctx, dto, true, false) + cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, true, false) if err != nil { return nil, err } @@ -253,12 +213,12 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt return dash, nil } -func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) { +func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *m.SaveDashboardDTO) (*models.Dashboard, error) { dto.User = &models.SignedInUser{ UserId: 0, OrgRole: models.ROLE_ADMIN, } - cmd, err := dr.buildSaveDashboardCommand(ctx, dto, false, false) + cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false, false) if err != nil { return nil, err } @@ -275,7 +235,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C return dash, nil } -func (dr *dashboardServiceImpl) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, +func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *m.SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", @@ -284,7 +244,7 @@ func (dr *dashboardServiceImpl) SaveDashboard(ctx context.Context, dto *SaveDash dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } - cmd, err := dr.buildSaveDashboardCommand(ctx, dto, true, !allowUiUpdate) + cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, true, !allowUiUpdate) if err != nil { return nil, err } @@ -303,16 +263,59 @@ func (dr *dashboardServiceImpl) SaveDashboard(ctx context.Context, dto *SaveDash // DeleteDashboard removes dashboard from the DB. Errors out if the dashboard was provisioned. Should be used for // operations by the user where we want to make sure user does not delete provisioned dashboard. -func (dr *dashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error { +func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error { return dr.deleteDashboard(ctx, dashboardId, orgId, true) } +func (dr *DashboardServiceImpl) MakeUserAdmin(ctx context.Context, orgID int64, userID int64, dashboardID int64, setViewAndEditPermissions bool) error { + rtEditor := models.ROLE_EDITOR + rtViewer := models.ROLE_VIEWER + + items := []*models.DashboardAcl{ + { + OrgID: orgID, + DashboardID: dashboardID, + UserID: userID, + Permission: models.PERMISSION_ADMIN, + Created: time.Now(), + Updated: time.Now(), + }, + } + + if setViewAndEditPermissions { + items = append(items, + &models.DashboardAcl{ + OrgID: orgID, + DashboardID: dashboardID, + Role: &rtEditor, + Permission: models.PERMISSION_EDIT, + Created: time.Now(), + Updated: time.Now(), + }, + &models.DashboardAcl{ + OrgID: orgID, + DashboardID: dashboardID, + Role: &rtViewer, + Permission: models.PERMISSION_VIEW, + Created: time.Now(), + Updated: time.Now(), + }, + ) + } + + if err := dr.dashboardStore.UpdateDashboardACL(ctx, dashboardID, items); err != nil { + return err + } + + return nil +} + // DeleteProvisionedDashboard removes dashboard from the DB even if it is provisioned. -func (dr *dashboardServiceImpl) DeleteProvisionedDashboard(ctx context.Context, dashboardId int64, orgId int64) error { +func (dr *DashboardServiceImpl) DeleteProvisionedDashboard(ctx context.Context, dashboardId int64, orgId int64) error { return dr.deleteDashboard(ctx, dashboardId, orgId, false) } -func (dr *dashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId int64, orgId int64, validateProvisionedDashboard bool) error { +func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId int64, orgId int64, validateProvisionedDashboard bool) error { if validateProvisionedDashboard { provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(dashboardId) if err != nil { @@ -327,7 +330,7 @@ func (dr *dashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId return bus.Dispatch(ctx, cmd) } -func (dr *dashboardServiceImpl) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) ( +func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *m.SaveDashboardDTO) ( *models.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", @@ -336,7 +339,7 @@ func (dr *dashboardServiceImpl) ImportDashboard(ctx context.Context, dto *SaveDa dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } - cmd, err := dr.buildSaveDashboardCommand(ctx, dto, false, true) + cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false, true) if err != nil { return nil, err } @@ -351,50 +354,6 @@ func (dr *dashboardServiceImpl) ImportDashboard(ctx context.Context, dto *SaveDa // UnprovisionDashboard removes info about dashboard being provisioned. Used after provisioning configs are changed // and provisioned dashboards are left behind but not deleted. -func (dr *dashboardServiceImpl) UnprovisionDashboard(ctx context.Context, dashboardId int64) error { - cmd := &models.UnprovisionDashboardCommand{Id: dashboardId} - return bus.Dispatch(ctx, cmd) -} - -type FakeDashboardService struct { - DashboardService - - SaveDashboardResult *models.Dashboard - SaveDashboardError error - SavedDashboards []*SaveDashboardDTO - ProvisionedDashData *models.DashboardProvisioning -} - -func (s *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) { - s.SavedDashboards = append(s.SavedDashboards, dto) - - if s.SaveDashboardResult == nil && s.SaveDashboardError == nil { - s.SaveDashboardResult = dto.Dashboard - } - - return s.SaveDashboardResult, s.SaveDashboardError -} - -func (s *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) { - return s.SaveDashboard(ctx, dto, true) -} - -func (s *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error { - for index, dash := range s.SavedDashboards { - if dash.Dashboard.Id == dashboardId && dash.OrgId == orgId { - s.SavedDashboards = append(s.SavedDashboards[:index], s.SavedDashboards[index+1:]...) - break - } - } - return nil -} - -func (s *FakeDashboardService) GetProvisionedDashboardDataByDashboardID(id int64) (*models.DashboardProvisioning, error) { - return s.ProvisionedDashData, nil -} - -func MockDashboardService(mock *FakeDashboardService) { - NewService = func(dashboards.Store) DashboardService { - return mock - } +func (dr *DashboardServiceImpl) UnprovisionDashboard(ctx context.Context, dashboardId int64) error { + return dr.dashboardStore.UnprovisionDashboard(ctx, dashboardId) } diff --git a/pkg/services/dashboards/dashboard_service_integration_test.go b/pkg/services/dashboards/manager/dashboard_service_integration_test.go similarity index 96% rename from pkg/services/dashboards/dashboard_service_integration_test.go rename to pkg/services/dashboards/manager/dashboard_service_integration_test.go index 8f6fba2d402..00bc7ca305b 100644 --- a/pkg/services/dashboards/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/manager/dashboard_service_integration_test.go @@ -1,20 +1,20 @@ //go:build integration // +build integration -package dashboards +package service import ( "context" "testing" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/dashboards" + "github.com/grafana/grafana/pkg/models" + dashbboardservice "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/models" ) const testOrgID int64 = 1 @@ -25,7 +25,7 @@ func TestIntegratedDashboardService(t *testing.T) { t.Cleanup(func() { UpdateAlerting = origUpdateAlerting }) - UpdateAlerting = func(ctx context.Context, store dashboards.Store, orgID int64, dashboard *models.Dashboard, user *models.SignedInUser) error { + UpdateAlerting = func(ctx context.Context, store dashbboardservice.Store, orgID int64, dashboard *models.Dashboard, user *models.SignedInUser) error { return nil } @@ -860,7 +860,8 @@ func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore t.Helper() dto := toSaveDashboardDto(cmd) - res, err := NewService(sqlStore).SaveDashboard(context.Background(), &dto, false) + dashboardStore := database.ProvideDashboardStore(sqlStore) + res, err := ProvideDashboardService(dashboardStore).SaveDashboard(context.Background(), &dto, false) require.NoError(t, err) return res @@ -868,7 +869,8 @@ func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore func callSaveWithError(cmd models.SaveDashboardCommand, sqlStore *sqlstore.SQLStore) error { dto := toSaveDashboardDto(cmd) - _, err := NewService(sqlStore).SaveDashboard(context.Background(), &dto, false) + dashboardStore := database.ProvideDashboardStore(sqlStore) + _, err := ProvideDashboardService(dashboardStore).SaveDashboard(context.Background(), &dto, false) return err } @@ -885,7 +887,7 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto }), } - dto := SaveDashboardDTO{ + dto := dashbboardservice.SaveDashboardDTO{ OrgId: orgID, Dashboard: cmd.GetDashboardModel(), User: &models.SignedInUser{ @@ -894,7 +896,8 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto }, } - res, err := NewService(sqlStore).SaveDashboard(context.Background(), &dto, false) + dashboardStore := database.ProvideDashboardStore(sqlStore) + res, err := ProvideDashboardService(dashboardStore).SaveDashboard(context.Background(), &dto, false) require.NoError(t, err) return res @@ -912,7 +915,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore *sqlstore. }), } - dto := SaveDashboardDTO{ + dto := dashbboardservice.SaveDashboardDTO{ OrgId: orgID, Dashboard: cmd.GetDashboardModel(), User: &models.SignedInUser{ @@ -921,16 +924,17 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore *sqlstore. }, } - res, err := NewService(sqlStore).SaveDashboard(context.Background(), &dto, false) + dashboardStore := database.ProvideDashboardStore(sqlStore) + res, err := ProvideDashboardService(dashboardStore).SaveDashboard(context.Background(), &dto, false) require.NoError(t, err) return res } -func toSaveDashboardDto(cmd models.SaveDashboardCommand) SaveDashboardDTO { +func toSaveDashboardDto(cmd models.SaveDashboardCommand) dashbboardservice.SaveDashboardDTO { dash := (&cmd).GetDashboardModel() - return SaveDashboardDTO{ + return dashbboardservice.SaveDashboardDTO{ Dashboard: dash, Message: cmd.Message, OrgId: cmd.OrgId, diff --git a/pkg/services/dashboards/dashboard_service_test.go b/pkg/services/dashboards/manager/dashboard_service_test.go similarity index 59% rename from pkg/services/dashboards/dashboard_service_test.go rename to pkg/services/dashboards/manager/dashboard_service_test.go index ec92ab0f6b7..1fb721e2e9f 100644 --- a/pkg/services/dashboards/dashboard_service_test.go +++ b/pkg/services/dashboards/manager/dashboard_service_test.go @@ -1,18 +1,22 @@ -package dashboards +//go:build integration +// +build integration + +package service import ( "context" - "fmt" + "errors" "testing" - "github.com/grafana/grafana/pkg/dashboards" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + m "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/guardian" - + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -20,8 +24,9 @@ func TestDashboardService(t *testing.T) { t.Run("Dashboard service tests", func(t *testing.T) { bus.ClearBusHandlers() - fakeStore := fakeDashboardStore{} - service := &dashboardServiceImpl{ + fakeStore := database.FakeDashboardStore{} + defer fakeStore.AssertExpectations(t) + service := &DashboardServiceImpl{ log: log.New("test.logger"), dashboardStore: &fakeStore, } @@ -31,7 +36,7 @@ func TestDashboardService(t *testing.T) { guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Run("Save dashboard validation", func(t *testing.T) { - dto := &SaveDashboardDTO{} + dto := &m.SaveDashboardDTO{} t.Run("When saving a dashboard with empty title it should return error", func(t *testing.T) { titles := []string{"", " ", " \t "} @@ -57,14 +62,6 @@ func TestDashboardService(t *testing.T) { }) t.Run("When saving a dashboard should validate uid", func(t *testing.T) { - origValidateAlerts := validateAlerts - t.Cleanup(func() { - validateAlerts = origValidateAlerts - }) - validateAlerts = func(ctx context.Context, dash *models.Dashboard, user *models.SignedInUser) error { - return nil - } - testCases := []struct { Uid string Error error @@ -83,24 +80,17 @@ func TestDashboardService(t *testing.T) { dto.Dashboard.SetUid(tc.Uid) dto.User = &models.SignedInUser{} - _, err := service.buildSaveDashboardCommand(context.Background(), dto, true, false) + if tc.Error == nil { + fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil).Once() + } + _, err := service.BuildSaveDashboardCommand(context.Background(), dto, true, false) require.Equal(t, err, tc.Error) } }) t.Run("Should return validation error if dashboard is provisioned", func(t *testing.T) { - t.Cleanup(func() { - fakeStore.provisionedData = nil - }) - fakeStore.provisionedData = &models.DashboardProvisioning{} - - origValidateAlerts := validateAlerts - t.Cleanup(func() { - validateAlerts = origValidateAlerts - }) - validateAlerts = func(ctx context.Context, dash *models.Dashboard, user *models.SignedInUser) error { - return nil - } + fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil).Once() + fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything).Return(&models.DashboardProvisioning{}, nil).Once() dto.Dashboard = models.NewDashboard("Dash") dto.Dashboard.SetId(3) @@ -110,13 +100,9 @@ func TestDashboardService(t *testing.T) { }) t.Run("Should not return validation error if dashboard is provisioned but UI updates allowed", func(t *testing.T) { - origValidateAlerts := validateAlerts - t.Cleanup(func() { - validateAlerts = origValidateAlerts - }) - validateAlerts = func(ctx context.Context, dash *models.Dashboard, user *models.SignedInUser) error { - return nil - } + fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil).Once() + fakeStore.On("SaveDashboard", mock.Anything).Return(&models.Dashboard{Data: simplejson.New()}, nil).Once() + fakeStore.On("SaveAlerts", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() dto.Dashboard = models.NewDashboard("Dash") dto.Dashboard.SetId(3) @@ -126,40 +112,25 @@ func TestDashboardService(t *testing.T) { }) t.Run("Should return validation error if alert data is invalid", func(t *testing.T) { - origValidateAlerts := validateAlerts - t.Cleanup(func() { - validateAlerts = origValidateAlerts - }) - validateAlerts = func(ctx context.Context, dash *models.Dashboard, user *models.SignedInUser) error { - return fmt.Errorf("alert validation error") - } + fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil).Once() + fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything).Return(nil, nil).Once() + fakeStore.On("SaveDashboard", mock.Anything).Return(&models.Dashboard{Data: simplejson.New()}, nil).Once() + fakeStore.On("SaveAlerts", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("alert validation error")).Once() dto.Dashboard = models.NewDashboard("Dash") + dto.User = &models.SignedInUser{UserId: 1} _, err := service.SaveDashboard(context.Background(), dto, false) require.Equal(t, err.Error(), "alert validation error") }) }) t.Run("Save provisioned dashboard validation", func(t *testing.T) { - dto := &SaveDashboardDTO{} + dto := &m.SaveDashboardDTO{} t.Run("Should not return validation error if dashboard is provisioned", func(t *testing.T) { - origUpdateAlerting := UpdateAlerting - t.Cleanup(func() { - UpdateAlerting = origUpdateAlerting - }) - UpdateAlerting = func(ctx context.Context, store dashboards.Store, orgID int64, dashboard *models.Dashboard, - user *models.SignedInUser) error { - return nil - } - - origValidateAlerts := validateAlerts - t.Cleanup(func() { - validateAlerts = origValidateAlerts - }) - validateAlerts = func(ctx context.Context, dash *models.Dashboard, user *models.SignedInUser) error { - return nil - } + fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil).Once() + fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{Data: simplejson.New()}, nil).Once() + fakeStore.On("SaveAlerts", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() dto.Dashboard = models.NewDashboard("Dash") dto.Dashboard.SetId(3) @@ -169,27 +140,14 @@ func TestDashboardService(t *testing.T) { }) t.Run("Should override invalid refresh interval if dashboard is provisioned", func(t *testing.T) { + fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil).Once() + fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{Data: simplejson.New()}, nil).Once() + fakeStore.On("SaveAlerts", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + oldRefreshInterval := setting.MinRefreshInterval setting.MinRefreshInterval = "5m" defer func() { setting.MinRefreshInterval = oldRefreshInterval }() - origValidateAlerts := validateAlerts - t.Cleanup(func() { - validateAlerts = origValidateAlerts - }) - validateAlerts = func(ctx context.Context, dash *models.Dashboard, user *models.SignedInUser) error { - return nil - } - - origUpdateAlerting := UpdateAlerting - t.Cleanup(func() { - UpdateAlerting = origUpdateAlerting - }) - UpdateAlerting = func(ctx context.Context, store dashboards.Store, orgID int64, dashboard *models.Dashboard, - user *models.SignedInUser) error { - return nil - } - dto.Dashboard = models.NewDashboard("Dash") dto.Dashboard.SetId(3) dto.User = &models.SignedInUser{UserId: 1} @@ -201,30 +159,11 @@ func TestDashboardService(t *testing.T) { }) t.Run("Import dashboard validation", func(t *testing.T) { - dto := &SaveDashboardDTO{} + dto := &m.SaveDashboardDTO{} t.Run("Should return validation error if dashboard is provisioned", func(t *testing.T) { - t.Cleanup(func() { - fakeStore.provisionedData = nil - }) - fakeStore.provisionedData = &models.DashboardProvisioning{} - - origValidateAlerts := validateAlerts - t.Cleanup(func() { - validateAlerts = origValidateAlerts - }) - validateAlerts = func(ctx context.Context, dash *models.Dashboard, user *models.SignedInUser) error { - return nil - } - - origUpdateAlerting := UpdateAlerting - t.Cleanup(func() { - UpdateAlerting = origUpdateAlerting - }) - UpdateAlerting = func(ctx context.Context, store dashboards.Store, orgID int64, dashboard *models.Dashboard, - user *models.SignedInUser) error { - return nil - } + fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil).Once() + fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything).Return(&models.DashboardProvisioning{}, nil).Once() dto.Dashboard = models.NewDashboard("Dash") dto.Dashboard.SetId(3) @@ -236,14 +175,15 @@ func TestDashboardService(t *testing.T) { t.Run("Given provisioned dashboard", func(t *testing.T) { t.Run("DeleteProvisionedDashboard should delete it", func(t *testing.T) { - result := setupDeleteHandlers(t, &fakeStore, true) + result := setupDeleteHandlers(t) err := service.DeleteProvisionedDashboard(context.Background(), 1, 1) require.NoError(t, err) require.True(t, result.deleteWasCalled) }) t.Run("DeleteDashboard should fail to delete it", func(t *testing.T) { - result := setupDeleteHandlers(t, &fakeStore, true) + fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything).Return(&models.DashboardProvisioning{}, nil).Once() + result := setupDeleteHandlers(t) err := service.DeleteDashboard(context.Background(), 1, 1) require.Equal(t, err, models.ErrDashboardCannotDeleteProvisionedDashboard) require.False(t, result.deleteWasCalled) @@ -251,7 +191,7 @@ func TestDashboardService(t *testing.T) { }) t.Run("Given non provisioned dashboard", func(t *testing.T) { - result := setupDeleteHandlers(t, &fakeStore, false) + result := setupDeleteHandlers(t) t.Run("DeleteProvisionedDashboard should delete it", func(t *testing.T) { err := service.DeleteProvisionedDashboard(context.Background(), 1, 1) @@ -260,6 +200,7 @@ func TestDashboardService(t *testing.T) { }) t.Run("DeleteDashboard should delete it", func(t *testing.T) { + fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything).Return(nil, nil).Once() err := service.DeleteDashboard(context.Background(), 1, 1) require.NoError(t, err) require.True(t, result.deleteWasCalled) @@ -272,16 +213,9 @@ type Result struct { deleteWasCalled bool } -func setupDeleteHandlers(t *testing.T, fakeStore *fakeDashboardStore, provisioned bool) *Result { +func setupDeleteHandlers(t *testing.T) *Result { t.Helper() - t.Cleanup(func() { - fakeStore.provisionedData = nil - }) - if provisioned { - fakeStore.provisionedData = &models.DashboardProvisioning{} - } - result := &Result{} bus.AddHandler("test", func(ctx context.Context, cmd *models.DeleteDashboardCommand) error { require.Equal(t, cmd.Id, int64(1)) @@ -292,32 +226,3 @@ func setupDeleteHandlers(t *testing.T, fakeStore *fakeDashboardStore, provisione return result } - -type fakeDashboardStore struct { - dashboards.Store - - validationError error - provisionedData *models.DashboardProvisioning -} - -func (s *fakeDashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) ( - bool, error) { - return false, s.validationError -} - -func (s *fakeDashboardStore) GetProvisionedDataByDashboardID(int64) (*models.DashboardProvisioning, error) { - return s.provisionedData, nil -} - -func (s *fakeDashboardStore) SaveProvisionedDashboard(models.SaveDashboardCommand, - *models.DashboardProvisioning) (*models.Dashboard, error) { - return nil, nil -} - -func (s *fakeDashboardStore) SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) { - return cmd.GetDashboardModel(), nil -} - -func (s *fakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error { - return nil -} diff --git a/pkg/services/dashboards/folder_service.go b/pkg/services/dashboards/manager/folder_service.go similarity index 55% rename from pkg/services/dashboards/folder_service.go rename to pkg/services/dashboards/manager/folder_service.go index b6f05b1e661..2876496bb12 100644 --- a/pkg/services/dashboards/folder_service.go +++ b/pkg/services/dashboards/manager/folder_service.go @@ -1,4 +1,4 @@ -package dashboards +package service import ( "context" @@ -6,46 +6,42 @@ import ( "strings" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/dashboards" + "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/guardian" "github.com/grafana/grafana/pkg/services/search" ) -// FolderService is a service for operating on folders. -type FolderService interface { - GetFolders(ctx context.Context, limit int64, page int64) ([]*models.Folder, error) - GetFolderByID(ctx context.Context, id int64) (*models.Folder, error) - GetFolderByUID(ctx context.Context, uid string) (*models.Folder, error) - GetFolderByTitle(ctx context.Context, title string) (*models.Folder, error) - CreateFolder(ctx context.Context, title, uid string) (*models.Folder, error) - UpdateFolder(ctx context.Context, uid string, cmd *models.UpdateFolderCommand) error - DeleteFolder(ctx context.Context, uid string, forceDeleteRules bool) (*models.Folder, error) - MakeUserAdmin(ctx context.Context, orgID int64, userID, folderID int64, setViewAndEditPermissions bool) error +type FolderServiceImpl struct { + dashboardService dashboards.DashboardService + dashboardStore dashboards.Store + searchService *search.SearchService + log log.Logger } -// NewFolderService is a factory for creating a new folder service. -var NewFolderService = func(orgID int64, user *models.SignedInUser, store dashboards.Store) FolderService { - return &dashboardServiceImpl{ - orgId: orgID, - user: user, - dashboardStore: store, +func ProvideFolderService(dashboardService dashboards.DashboardService, dashboardStore dashboards.Store, searchService *search.SearchService) *FolderServiceImpl { + return &FolderServiceImpl{ + dashboardService: dashboardService, + dashboardStore: dashboardStore, + searchService: searchService, + log: log.New("folder-service"), } } -func (dr *dashboardServiceImpl) GetFolders(ctx context.Context, limit int64, page int64) ([]*models.Folder, error) { +func (f *FolderServiceImpl) GetFolders(ctx context.Context, user *models.SignedInUser, orgID int64, limit int64, page int64) ([]*models.Folder, error) { searchQuery := search.Query{ - SignedInUser: dr.user, + SignedInUser: user, DashboardIds: make([]int64, 0), FolderIds: make([]int64, 0), Limit: limit, - OrgId: dr.orgId, + OrgId: orgID, Type: "dash-folder", Permission: models.PERMISSION_VIEW, Page: page, } - if err := bus.Dispatch(ctx, &searchQuery); err != nil { + if err := f.searchService.SearchHandler(ctx, &searchQuery); err != nil { return nil, err } @@ -62,17 +58,17 @@ func (dr *dashboardServiceImpl) GetFolders(ctx context.Context, limit int64, pag return folders, nil } -func (dr *dashboardServiceImpl) GetFolderByID(ctx context.Context, id int64) (*models.Folder, error) { +func (f *FolderServiceImpl) GetFolderByID(ctx context.Context, user *models.SignedInUser, id int64, orgID int64) (*models.Folder, error) { if id == 0 { return &models.Folder{Id: id, Title: "General"}, nil } - query := models.GetDashboardQuery{OrgId: dr.orgId, Id: id} + query := models.GetDashboardQuery{OrgId: orgID, Id: id} dashFolder, err := getFolder(ctx, query) if err != nil { return nil, toFolderError(err) } - g := guardian.New(ctx, dashFolder.Id, dr.orgId, dr.user) + g := guardian.New(ctx, dashFolder.Id, orgID, user) if canView, err := g.CanView(); err != nil || !canView { if err != nil { return nil, toFolderError(err) @@ -83,15 +79,15 @@ func (dr *dashboardServiceImpl) GetFolderByID(ctx context.Context, id int64) (*m return dashToFolder(dashFolder), nil } -func (dr *dashboardServiceImpl) GetFolderByUID(ctx context.Context, uid string) (*models.Folder, error) { - query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid} +func (f *FolderServiceImpl) GetFolderByUID(ctx context.Context, user *models.SignedInUser, orgID int64, uid string) (*models.Folder, error) { + query := models.GetDashboardQuery{OrgId: orgID, Uid: uid} dashFolder, err := getFolder(ctx, query) if err != nil { return nil, toFolderError(err) } - g := guardian.New(ctx, dashFolder.Id, dr.orgId, dr.user) + g := guardian.New(ctx, dashFolder.Id, orgID, user) if canView, err := g.CanView(); err != nil || !canView { if err != nil { return nil, toFolderError(err) @@ -102,13 +98,13 @@ func (dr *dashboardServiceImpl) GetFolderByUID(ctx context.Context, uid string) return dashToFolder(dashFolder), nil } -func (dr *dashboardServiceImpl) GetFolderByTitle(ctx context.Context, title string) (*models.Folder, error) { - dashFolder, err := dr.dashboardStore.GetFolderByTitle(dr.orgId, title) +func (f *FolderServiceImpl) GetFolderByTitle(ctx context.Context, user *models.SignedInUser, orgID int64, title string) (*models.Folder, error) { + dashFolder, err := f.dashboardStore.GetFolderByTitle(orgID, title) if err != nil { return nil, toFolderError(err) } - g := guardian.New(ctx, dashFolder.Id, dr.orgId, dr.user) + g := guardian.New(ctx, dashFolder.Id, orgID, user) if canView, err := g.CanView(); err != nil || !canView { if err != nil { return nil, toFolderError(err) @@ -119,11 +115,11 @@ func (dr *dashboardServiceImpl) GetFolderByTitle(ctx context.Context, title stri return dashToFolder(dashFolder), nil } -func (dr *dashboardServiceImpl) CreateFolder(ctx context.Context, title, uid string) (*models.Folder, error) { +func (f *FolderServiceImpl) CreateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, title, uid string) (*models.Folder, error) { dashFolder := models.NewDashboardFolder(title) - dashFolder.OrgId = dr.orgId + dashFolder.OrgId = orgID dashFolder.SetUid(strings.TrimSpace(uid)) - userID := dr.user.UserId + userID := user.UserId if userID == 0 { userID = -1 } @@ -131,23 +127,23 @@ func (dr *dashboardServiceImpl) CreateFolder(ctx context.Context, title, uid str dashFolder.UpdatedBy = userID dashFolder.UpdateSlug() - dto := &SaveDashboardDTO{ + dto := &dashboards.SaveDashboardDTO{ Dashboard: dashFolder, - OrgId: dr.orgId, - User: dr.user, + OrgId: orgID, + User: user, } - saveDashboardCmd, err := dr.buildSaveDashboardCommand(ctx, dto, false, false) + saveDashboardCmd, err := f.dashboardService.BuildSaveDashboardCommand(ctx, dto, false, false) if err != nil { return nil, toFolderError(err) } - dash, err := dr.dashboardStore.SaveDashboard(*saveDashboardCmd) + dash, err := f.dashboardStore.SaveDashboard(*saveDashboardCmd) if err != nil { return nil, toFolderError(err) } - query := models.GetDashboardQuery{OrgId: dr.orgId, Id: dash.Id} + query := models.GetDashboardQuery{OrgId: orgID, Id: dash.Id} dashFolder, err = getFolder(ctx, query) if err != nil { return nil, toFolderError(err) @@ -156,33 +152,33 @@ func (dr *dashboardServiceImpl) CreateFolder(ctx context.Context, title, uid str return dashToFolder(dashFolder), nil } -func (dr *dashboardServiceImpl) UpdateFolder(ctx context.Context, existingUid string, cmd *models.UpdateFolderCommand) error { - query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: existingUid} +func (f *FolderServiceImpl) UpdateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) error { + query := models.GetDashboardQuery{OrgId: orgID, Uid: existingUid} dashFolder, err := getFolder(ctx, query) if err != nil { return toFolderError(err) } - cmd.UpdateDashboardModel(dashFolder, dr.orgId, dr.user.UserId) + cmd.UpdateDashboardModel(dashFolder, orgID, user.UserId) - dto := &SaveDashboardDTO{ + dto := &dashboards.SaveDashboardDTO{ Dashboard: dashFolder, - OrgId: dr.orgId, - User: dr.user, + OrgId: orgID, + User: user, Overwrite: cmd.Overwrite, } - saveDashboardCmd, err := dr.buildSaveDashboardCommand(ctx, dto, false, false) + saveDashboardCmd, err := f.dashboardService.BuildSaveDashboardCommand(ctx, dto, false, false) if err != nil { return toFolderError(err) } - dash, err := dr.dashboardStore.SaveDashboard(*saveDashboardCmd) + dash, err := f.dashboardStore.SaveDashboard(*saveDashboardCmd) if err != nil { return toFolderError(err) } - query = models.GetDashboardQuery{OrgId: dr.orgId, Id: dash.Id} + query = models.GetDashboardQuery{OrgId: orgID, Id: dash.Id} dashFolder, err = getFolder(ctx, query) if err != nil { return toFolderError(err) @@ -193,14 +189,14 @@ func (dr *dashboardServiceImpl) UpdateFolder(ctx context.Context, existingUid st return nil } -func (dr *dashboardServiceImpl) DeleteFolder(ctx context.Context, uid string, forceDeleteRules bool) (*models.Folder, error) { - query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid} +func (f *FolderServiceImpl) DeleteFolder(ctx context.Context, user *models.SignedInUser, orgID int64, uid string, forceDeleteRules bool) (*models.Folder, error) { + query := models.GetDashboardQuery{OrgId: orgID, Uid: uid} dashFolder, err := getFolder(ctx, query) if err != nil { return nil, toFolderError(err) } - guardian := guardian.New(ctx, dashFolder.Id, dr.orgId, dr.user) + guardian := guardian.New(ctx, dashFolder.Id, orgID, user) if canSave, err := guardian.CanSave(); err != nil || !canSave { if err != nil { return nil, toFolderError(err) @@ -208,7 +204,7 @@ func (dr *dashboardServiceImpl) DeleteFolder(ctx context.Context, uid string, fo return nil, models.ErrFolderAccessDenied } - deleteCmd := models.DeleteDashboardCommand{OrgId: dr.orgId, Id: dashFolder.Id, ForceDeleteFolderRules: forceDeleteRules} + deleteCmd := models.DeleteDashboardCommand{OrgId: orgID, Id: dashFolder.Id, ForceDeleteFolderRules: forceDeleteRules} if err := bus.Dispatch(ctx, &deleteCmd); err != nil { return nil, toFolderError(err) } @@ -216,6 +212,10 @@ func (dr *dashboardServiceImpl) DeleteFolder(ctx context.Context, uid string, fo return dashToFolder(dashFolder), nil } +func (f *FolderServiceImpl) MakeUserAdmin(ctx context.Context, orgID int64, userID, folderID int64, setViewAndEditPermissions bool) error { + return f.dashboardService.MakeUserAdmin(ctx, orgID, userID, folderID, setViewAndEditPermissions) +} + func getFolder(ctx context.Context, query models.GetDashboardQuery) (*models.Dashboard, error) { if err := bus.Dispatch(ctx, &query); err != nil { return nil, toFolderError(err) diff --git a/pkg/services/dashboards/folder_service_test.go b/pkg/services/dashboards/manager/folder_service_test.go similarity index 73% rename from pkg/services/dashboards/folder_service_test.go rename to pkg/services/dashboards/manager/folder_service_test.go index 7d08b6a6bf7..962e0fe88de 100644 --- a/pkg/services/dashboards/folder_service_test.go +++ b/pkg/services/dashboards/manager/folder_service_test.go @@ -1,25 +1,34 @@ -package dashboards +//go:build integration +// +build integration + +package service import ( "context" "testing" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/models" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) +var orgID = int64(1) +var user = &models.SignedInUser{UserId: 1} + func TestFolderService(t *testing.T) { t.Run("Folder service tests", func(t *testing.T) { - service := dashboardServiceImpl{ - orgId: 1, - user: &models.SignedInUser{UserId: 1}, - dashboardStore: &fakeDashboardStore{}, - } + store := &database.FakeDashboardStore{} + defer store.AssertExpectations(t) + service := ProvideFolderService( + &dashboards.FakeDashboardService{DashboardService: ProvideDashboardService(store)}, + store, + nil, + ) t.Run("Given user has no permissions", func(t *testing.T) { origNewGuardian := guardian.New @@ -30,37 +39,30 @@ func TestFolderService(t *testing.T) { return nil }) - origStore := service.dashboardStore - t.Cleanup(func() { - service.dashboardStore = origStore - }) - service.dashboardStore = &fakeDashboardStore{ - validationError: models.ErrDashboardUpdateAccessDenied, - } - t.Run("When get folder by id should return access denied error", func(t *testing.T) { - _, err := service.GetFolderByID(context.Background(), 1) + _, err := service.GetFolderByID(context.Background(), user, 1, orgID) require.Equal(t, err, models.ErrFolderAccessDenied) }) t.Run("When get folder by id, with id = 0 should return default folder", func(t *testing.T) { - folder, err := service.GetFolderByID(context.Background(), 0) + folder, err := service.GetFolderByID(context.Background(), user, 0, orgID) require.NoError(t, err) require.Equal(t, folder, &models.Folder{Id: 0, Title: "General"}) }) t.Run("When get folder by uid should return access denied error", func(t *testing.T) { - _, err := service.GetFolderByUID(context.Background(), "uid") + _, err := service.GetFolderByUID(context.Background(), user, orgID, "uid") require.Equal(t, err, models.ErrFolderAccessDenied) }) t.Run("When creating folder should return access denied error", func(t *testing.T) { - _, err := service.CreateFolder(context.Background(), "Folder", "") + store.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil).Times(2) + _, err := service.CreateFolder(context.Background(), user, orgID, "Folder", "") require.Equal(t, err, models.ErrFolderAccessDenied) }) t.Run("When updating folder should return access denied error", func(t *testing.T) { - err := service.UpdateFolder(context.Background(), "uid", &models.UpdateFolderCommand{ + err := service.UpdateFolder(context.Background(), user, orgID, "uid", &models.UpdateFolderCommand{ Uid: "uid", Title: "Folder", }) @@ -68,7 +70,7 @@ func TestFolderService(t *testing.T) { }) t.Run("When deleting folder by uid should return access denied error", func(t *testing.T) { - _, err := service.DeleteFolder(context.Background(), "uid", false) + _, err := service.DeleteFolder(context.Background(), user, orgID, "uid", false) require.Error(t, err) require.Equal(t, err, models.ErrFolderAccessDenied) }) @@ -90,15 +92,6 @@ func TestFolderService(t *testing.T) { return nil }) - origUpdateAlerting := UpdateAlerting - t.Cleanup(func() { - UpdateAlerting = origUpdateAlerting - }) - UpdateAlerting = func(ctx context.Context, store dashboards.Store, orgID int64, dashboard *models.Dashboard, - user *models.SignedInUser) error { - return nil - } - bus.AddHandler("test", func(ctx context.Context, cmd *models.SaveDashboardCommand) error { cmd.Result = dash return nil @@ -109,12 +102,15 @@ func TestFolderService(t *testing.T) { }) t.Run("When creating folder should not return access denied error", func(t *testing.T) { - _, err := service.CreateFolder(context.Background(), "Folder", "") + store.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil).Times(2) + store.On("SaveDashboard", mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once() + _, err := service.CreateFolder(context.Background(), user, orgID, "Folder", "") require.NoError(t, err) }) t.Run("When updating folder should not return access denied error", func(t *testing.T) { - err := service.UpdateFolder(context.Background(), "uid", &models.UpdateFolderCommand{ + store.On("SaveDashboard", mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once() + err := service.UpdateFolder(context.Background(), user, orgID, "uid", &models.UpdateFolderCommand{ Uid: "uid", Title: "Folder", }) @@ -122,7 +118,7 @@ func TestFolderService(t *testing.T) { }) t.Run("When deleting folder by uid should not return access denied error", func(t *testing.T) { - _, err := service.DeleteFolder(context.Background(), "uid", false) + _, err := service.DeleteFolder(context.Background(), user, orgID, "uid", false) require.NoError(t, err) }) @@ -145,14 +141,14 @@ func TestFolderService(t *testing.T) { }) t.Run("When get folder by id should return folder", func(t *testing.T) { - f, _ := service.GetFolderByID(context.Background(), 1) + f, _ := service.GetFolderByID(context.Background(), user, orgID, 1) require.Equal(t, f.Id, dashFolder.Id) require.Equal(t, f.Uid, dashFolder.Uid) require.Equal(t, f.Title, dashFolder.Title) }) t.Run("When get folder by uid should return folder", func(t *testing.T) { - f, _ := service.GetFolderByUID(context.Background(), "uid") + f, _ := service.GetFolderByUID(context.Background(), user, orgID, "uid") require.Equal(t, f.Id, dashFolder.Id) require.Equal(t, f.Uid, dashFolder.Uid) require.Equal(t, f.Title, dashFolder.Title) diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go new file mode 100644 index 00000000000..eea9e9c9b44 --- /dev/null +++ b/pkg/services/dashboards/models.go @@ -0,0 +1,16 @@ +package dashboards + +import ( + "time" + + "github.com/grafana/grafana/pkg/models" +) + +type SaveDashboardDTO struct { + OrgId int64 + UpdatedAt time.Time + User *models.SignedInUser + Message string + Overwrite bool + Dashboard *models.Dashboard +} diff --git a/pkg/services/libraryelements/guard.go b/pkg/services/libraryelements/guard.go index 8d483488140..7265e3acc3d 100644 --- a/pkg/services/libraryelements/guard.go +++ b/pkg/services/libraryelements/guard.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" ) @@ -32,9 +31,7 @@ func (l *LibraryElementService) requirePermissionsOnFolder(ctx context.Context, if isGeneralFolder(folderID) && user.HasRole(models.ROLE_VIEWER) { return models.ErrFolderAccessDenied } - - s := dashboards.NewFolderService(user.OrgId, user, l.SQLStore) - folder, err := s.GetFolderByID(ctx, folderID) + folder, err := l.folderService.GetFolderByID(ctx, user, folderID, user.OrgId) if err != nil { return err } diff --git a/pkg/services/libraryelements/libraryelements.go b/pkg/services/libraryelements/libraryelements.go index 302b31a1b59..50229a71b68 100644 --- a/pkg/services/libraryelements/libraryelements.go +++ b/pkg/services/libraryelements/libraryelements.go @@ -6,15 +6,17 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "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/sqlstore" "github.com/grafana/grafana/pkg/setting" ) -func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, routeRegister routing.RouteRegister) *LibraryElementService { +func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, routeRegister routing.RouteRegister, folderService dashboards.FolderService) *LibraryElementService { l := &LibraryElementService{ Cfg: cfg, SQLStore: sqlStore, RouteRegister: routeRegister, + folderService: folderService, log: log.New("library-elements"), } l.registerAPIEndpoints() @@ -36,6 +38,7 @@ type LibraryElementService struct { Cfg *setting.Cfg SQLStore *sqlstore.SQLStore RouteRegister routing.RouteRegister + folderService dashboards.FolderService log log.Logger } diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 5744f9c8b5e..8c1df027581 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -9,14 +9,13 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/components/simplejson" - - dboards "github.com/grafana/grafana/pkg/dashboards" - "github.com/google/go-cmp/cmp" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/database" + dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" @@ -194,16 +193,9 @@ func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.Sign User: &user, Overwrite: false, } - origUpdateAlerting := dashboards.UpdateAlerting - t.Cleanup(func() { - dashboards.UpdateAlerting = origUpdateAlerting - }) - dashboards.UpdateAlerting = func(ctx context.Context, store dboards.Store, orgID int64, dashboard *models.Dashboard, - user *models.SignedInUser) error { - return nil - } - dashboard, err := dashboards.NewService(sqlStore).SaveDashboard(context.Background(), dashItem, true) + dashboardStore := database.ProvideDashboardStore(sqlStore) + dashboard, err := dashboardservice.ProvideDashboardService(dashboardStore).SaveDashboard(context.Background(), dashItem, true) require.NoError(t, err) return dashboard @@ -213,17 +205,19 @@ func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string items []folderACLItem) *models.Folder { t.Helper() - s := dashboards.NewFolderService(user.OrgId, &user, sqlStore) + dashboardStore := database.ProvideDashboardStore(sqlStore) + d := dashboardservice.ProvideDashboardService(dashboardStore) + s := dashboardservice.ProvideFolderService(d, dashboardStore, nil) t.Logf("Creating folder with title and UID %q", title) - folder, err := s.CreateFolder(context.Background(), title, title) + folder, err := s.CreateFolder(context.Background(), &user, user.OrgId, title, title) require.NoError(t, err) - updateFolderACL(t, sqlStore, folder.Id, items) + updateFolderACL(t, dashboardStore, folder.Id, items) return folder } -func updateFolderACL(t *testing.T, sqlStore *sqlstore.SQLStore, folderID int64, items []folderACLItem) { +func updateFolderACL(t *testing.T, dashboardStore *database.DashboardStore, folderID int64, items []folderACLItem) { t.Helper() if len(items) == 0 { @@ -243,7 +237,7 @@ func updateFolderACL(t *testing.T, sqlStore *sqlstore.SQLStore, folderID int64, }) } - err := sqlStore.UpdateDashboardACL(context.Background(), folderID, aclItems) + err := dashboardStore.UpdateDashboardACL(context.Background(), folderID, aclItems) require.NoError(t, err) } @@ -297,9 +291,12 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo orgID := int64(1) role := models.ROLE_ADMIN sqlStore := sqlstore.InitTestDB(t) + dashboardStore := database.ProvideDashboardStore(sqlStore) + dashboardService := dashboardservice.ProvideDashboardService(dashboardStore) service := LibraryElementService{ - Cfg: setting.NewCfg(), - SQLStore: sqlStore, + Cfg: setting.NewCfg(), + SQLStore: sqlStore, + folderService: dashboardservice.ProvideFolderService(dashboardService, dashboardStore, nil), } user := models.SignedInUser{ diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 2a73e01e632..7a101269709 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -8,15 +8,16 @@ import ( "time" "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/require" - + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" - dboards "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/database" + dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/require" ) const userInDbName = "user_in_db" @@ -1413,16 +1414,9 @@ func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user *models.Sig User: user, Overwrite: false, } - origUpdateAlerting := dashboards.UpdateAlerting - t.Cleanup(func() { - dashboards.UpdateAlerting = origUpdateAlerting - }) - dashboards.UpdateAlerting = func(ctx context.Context, store dboards.Store, orgID int64, dashboard *models.Dashboard, - user *models.SignedInUser) error { - return nil - } - dashboard, err := dashboards.NewService(sqlStore).SaveDashboard(context.Background(), dashItem, true) + dashboadStore := database.ProvideDashboardStore(sqlStore) + dashboard, err := dashboardservice.ProvideDashboardService(dashboadStore).SaveDashboard(context.Background(), dashItem, true) require.NoError(t, err) return dashboard @@ -1432,17 +1426,19 @@ func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string items []folderACLItem) *models.Folder { t.Helper() - s := dashboards.NewFolderService(user.OrgId, user, sqlStore) + dashboardStore := database.ProvideDashboardStore(sqlStore) + d := dashboardservice.ProvideDashboardService(dashboardStore) + s := dashboardservice.ProvideFolderService(d, dashboardStore, nil) t.Logf("Creating folder with title and UID %q", title) - folder, err := s.CreateFolder(context.Background(), title, title) + folder, err := s.CreateFolder(context.Background(), user, user.OrgId, title, title) require.NoError(t, err) - updateFolderACL(t, sqlStore, folder.Id, items) + updateFolderACL(t, dashboardStore, folder.Id, items) return folder } -func updateFolderACL(t *testing.T, sqlStore *sqlstore.SQLStore, folderID int64, items []folderACLItem) { +func updateFolderACL(t *testing.T, dashboardStore *database.DashboardStore, folderID int64, items []folderACLItem) { t.Helper() if len(items) == 0 { @@ -1462,7 +1458,7 @@ func updateFolderACL(t *testing.T, sqlStore *sqlstore.SQLStore, folderID int64, }) } - err := sqlStore.UpdateDashboardACL(context.Background(), folderID, aclItems) + err := dashboardStore.UpdateDashboardACL(context.Background(), folderID, aclItems) require.NoError(t, err) } @@ -1519,14 +1515,14 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo orgID := int64(1) role := models.ROLE_ADMIN sqlStore := sqlstore.InitTestDB(t) - elementService := libraryelements.LibraryElementService{ - Cfg: cfg, - SQLStore: sqlStore, - } + dashboardStore := database.ProvideDashboardStore(sqlStore) + folderService := dashboardservice.ProvideFolderService(dashboardservice.ProvideDashboardService(dashboardStore), dashboardStore, nil) + + elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService) service := LibraryPanelService{ Cfg: cfg, SQLStore: sqlStore, - LibraryElementService: &elementService, + LibraryElementService: elementService, } user := &models.SignedInUser{ @@ -1555,7 +1551,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo user: user, ctx: context.Background(), service: &service, - elementService: &elementService, + elementService: elementService, sqlStore: sqlStore, } diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 53162c1fa13..346a6e5f055 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -5,12 +5,11 @@ import ( "net/url" "github.com/benbjohnson/clock" - "golang.org/x/sync/errgroup" - "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/ngalert/api" @@ -25,11 +24,12 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" + "golang.org/x/sync/errgroup" ) func ProvideService(cfg *setting.Cfg, dataSourceCache datasources.CacheService, routeRegister routing.RouteRegister, sqlStore *sqlstore.SQLStore, kvStore kvstore.KVStore, expressionService *expr.Service, dataProxy *datasourceproxy.DataSourceProxyService, - quotaService *quota.QuotaService, secretsService secrets.Service, notificationService notifications.Service, m *metrics.NGAlert) (*AlertNG, error) { + quotaService *quota.QuotaService, secretsService secrets.Service, notificationService notifications.Service, m *metrics.NGAlert, folderService dashboards.FolderService) (*AlertNG, error) { ng := &AlertNG{ Cfg: cfg, DataSourceCache: dataSourceCache, @@ -41,8 +41,9 @@ func ProvideService(cfg *setting.Cfg, dataSourceCache datasources.CacheService, QuotaService: quotaService, SecretsService: secretsService, Metrics: m, - NotificationService: notificationService, Log: log.New("ngalert"), + NotificationService: notificationService, + folderService: folderService, } if ng.IsDisabled() { @@ -72,6 +73,7 @@ type AlertNG struct { Log log.Logger schedule schedule.ScheduleService stateManager *state.Manager + folderService dashboards.FolderService // Alerting notification services MultiOrgAlertmanager *notifier.MultiOrgAlertmanager @@ -85,6 +87,7 @@ func (ng *AlertNG) init() error { DefaultInterval: ng.Cfg.UnifiedAlerting.DefaultAlertForDuration, SQLStore: ng.SQLStore, Logger: ng.Log, + FolderService: ng.folderService, } decryptFn := ng.SecretsService.GetDecryptedValue diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index a0a16ec7871..dead82108a1 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -12,8 +12,6 @@ import ( "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/dashboards" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -395,12 +393,11 @@ func (st DBstore) GetRuleGroupAlertRules(ctx context.Context, query *ngmodels.Li // GetNamespaces returns the folders that are visible to the user func (st DBstore) GetNamespaces(ctx context.Context, orgID int64, user *models.SignedInUser) (map[string]*models.Folder, error) { - s := dashboards.NewFolderService(orgID, user, st.SQLStore) namespaceMap := make(map[string]*models.Folder) var page int64 = 1 for { // if limit is negative; it fetches at most 1000 - folders, err := s.GetFolders(ctx, -1, page) + folders, err := st.FolderService.GetFolders(ctx, user, orgID, -1, page) if err != nil { return nil, err } @@ -419,8 +416,7 @@ func (st DBstore) GetNamespaces(ctx context.Context, orgID int64, user *models.S // GetNamespaceByTitle is a handler for retrieving a namespace by its title. Alerting rules follow a Grafana folder-like structure which we call namespaces. func (st DBstore) GetNamespaceByTitle(ctx context.Context, namespace string, orgID int64, user *models.SignedInUser, withCanSave bool) (*models.Folder, error) { - s := dashboards.NewFolderService(orgID, user, st.SQLStore) - folder, err := s.GetFolderByTitle(ctx, namespace) + folder, err := st.FolderService.GetFolderByTitle(ctx, user, orgID, namespace) if err != nil { return nil, err } diff --git a/pkg/services/ngalert/store/database.go b/pkg/services/ngalert/store/database.go index 7a6505704e7..aa0e601d632 100644 --- a/pkg/services/ngalert/store/database.go +++ b/pkg/services/ngalert/store/database.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/sqlstore" ) @@ -31,4 +32,5 @@ type DBstore struct { DefaultInterval time.Duration SQLStore *sqlstore.SQLStore Logger log.Logger + FolderService dashboards.FolderService } diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index b017436111e..b0af3a96e34 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -9,6 +9,8 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" + databasestore "github.com/grafana/grafana/pkg/services/dashboards/database" + dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/services/ngalert" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -40,9 +42,11 @@ func SetupTestEnv(t *testing.T, baseInterval time.Duration) (*ngalert.AlertNG, * m := metrics.NewNGAlert(prometheus.NewRegistry()) sqlStore := sqlstore.InitTestDB(t) secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) + dashboardStore := databasestore.ProvideDashboardStore(sqlStore) + folderService := dashboardservice.ProvideFolderService(dashboardservice.ProvideDashboardService(dashboardStore), dashboardStore, nil) ng, err := ngalert.ProvideService( cfg, nil, routing.NewRouteRegister(), sqlStore, - nil, nil, nil, nil, secretsService, nil, m, + nil, nil, nil, nil, secretsService, nil, m, folderService, ) require.NoError(t, err) return ng, &store.DBstore{ diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index f5d0a4dc08e..fbdf6386dba 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -6,9 +6,9 @@ import ( "os" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/dashboards" "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/util/errutil" ) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 9f1bedf2f81..a63a5e045e7 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -13,10 +13,10 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" - dboards "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/util" ) @@ -41,7 +41,7 @@ type FileReader struct { } // NewDashboardFileReader returns a new filereader based on `config` -func NewDashboardFileReader(cfg *config, log log.Logger, store dboards.Store) (*FileReader, error) { +func NewDashboardFileReader(cfg *config, log log.Logger, store dashboards.Store) (*FileReader, error) { var path string path, ok := cfg.Options["path"].(string) if !ok { @@ -62,7 +62,7 @@ func NewDashboardFileReader(cfg *config, log log.Logger, store dboards.Store) (* Cfg: cfg, Path: path, log: log, - dashboardProvisioningService: dashboards.NewProvisioningService(store), + dashboardProvisioningService: dashboardservice.ProvideDashboardService(store), FoldersFromFilesStructure: foldersFromFilesStructure, usageTracker: newUsageTracker(), }, nil diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index 70c7ad3cdae..810b3a8d284 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -2,8 +2,6 @@ package dashboards import ( "context" - "fmt" - "math/rand" "os" "path/filepath" "runtime" @@ -11,13 +9,12 @@ import ( "time" "github.com/grafana/grafana/pkg/bus" - dboards "github.com/grafana/grafana/pkg/dashboards" + "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/util" - - "github.com/grafana/grafana/pkg/infra/log" - + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -28,10 +25,9 @@ const ( containingID = "testdata/test-dashboards/containing-id" unprovision = "testdata/test-dashboards/unprovision" foldersFromFilesStructure = "testdata/test-dashboards/folders-from-files-structure" + configName = "default" ) -var fakeService *fakeDashboardProvisioningService - func TestCreatingNewDashboardFileReader(t *testing.T) { setup := func() *config { return &config{ @@ -98,17 +94,14 @@ func TestDashboardFileReader(t *testing.T) { logger := log.New("test.logger") cfg := &config{} - origNewDashboardProvisioningService := dashboards.NewProvisioningService - defer func() { - dashboards.NewProvisioningService = origNewDashboardProvisioningService - }() + fakeService := &dashboards.FakeDashboardProvisioning{} + defer fakeService.AssertExpectations(t) setup := func() { bus.ClearBusHandlers() - fakeService = mockDashboardProvisioningService() bus.AddHandler("test", mockGetDashboardQuery) cfg = &config{ - Name: "Default", + Name: configName, Type: "file", OrgID: 1, Folder: "", @@ -122,45 +115,38 @@ func TestDashboardFileReader(t *testing.T) { cfg.Options["path"] = defaultDashboards cfg.Folder = "Team A" + fakeService.On("GetProvisionedDashboardData", configName).Return(nil, nil).Once() + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 2}, nil).Times(2) + reader, err := NewDashboardFileReader(cfg, logger, nil) + reader.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader.walkDisk(context.Background()) require.NoError(t, err) - - folders := 0 - dashboards := 0 - - for _, i := range fakeService.inserted { - if i.Dashboard.IsFolder { - folders++ - } else { - dashboards++ - } - } - - require.Equal(t, folders, 1) - require.Equal(t, dashboards, 2) }) t.Run("Can read default dashboard and replace old version in database", func(t *testing.T) { setup() cfg.Options["path"] = oneDashboard - stat, _ := os.Stat(oneDashboard + "/dashboard1.json") - - fakeService.getDashboard = append(fakeService.getDashboard, &models.Dashboard{ - Updated: stat.ModTime().AddDate(0, 0, -1), - Slug: "grafana", - }) + inserted := 0 + fakeService.On("GetProvisionedDashboardData", configName).Return(nil, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything). + Return(&models.Dashboard{}, nil).Once(). + Run(func(args mock.Arguments) { + inserted++ + }) reader, err := NewDashboardFileReader(cfg, logger, nil) + reader.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader.walkDisk(context.Background()) require.NoError(t, err) - require.Equal(t, len(fakeService.inserted), 1) + assert.Equal(t, inserted, 1) }) t.Run("Dashboard with older timestamp and the same checksum will not replace imported dashboard", func(t *testing.T) { @@ -179,23 +165,23 @@ func TestDashboardFileReader(t *testing.T) { checksum, err := util.Md5Sum(file) require.NoError(t, err) - fakeService.provisioned = map[string][]*models.DashboardProvisioning{ - "Default": { - { - Name: "Default", - ExternalId: absPath, - Updated: stat.ModTime().AddDate(0, 0, +1).Unix(), - CheckSum: checksum, - }, + provisionedDashboard := []*models.DashboardProvisioning{ + { + Name: "Default", + ExternalId: absPath, + Updated: stat.ModTime().AddDate(0, 0, +1).Unix(), + CheckSum: checksum, }, } + fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() + reader, err := NewDashboardFileReader(cfg, logger, nil) + reader.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader.walkDisk(context.Background()) require.NoError(t, err) - require.Equal(t, len(fakeService.inserted), 0) }) t.Run("Dashboard with older timestamp and different checksum will replace imported dashboard", func(t *testing.T) { @@ -206,23 +192,24 @@ func TestDashboardFileReader(t *testing.T) { stat, err := os.Stat(oneDashboard + "/dashboard1.json") require.NoError(t, err) - fakeService.provisioned = map[string][]*models.DashboardProvisioning{ - "Default": { - { - Name: "Default", - ExternalId: absPath, - Updated: stat.ModTime().AddDate(0, 0, +1).Unix(), - CheckSum: "fakechecksum", - }, + provisionedDashboard := []*models.DashboardProvisioning{ + { + Name: "Default", + ExternalId: absPath, + Updated: stat.ModTime().AddDate(0, 0, +1).Unix(), + CheckSum: "fakechecksum", }, } + fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + reader, err := NewDashboardFileReader(cfg, logger, nil) + reader.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader.walkDisk(context.Background()) require.NoError(t, err) - require.Equal(t, len(fakeService.inserted), 1) }) t.Run("Dashboard with newer timestamp and the same checksum will not replace imported dashboard", func(t *testing.T) { @@ -241,23 +228,23 @@ func TestDashboardFileReader(t *testing.T) { checksum, err := util.Md5Sum(file) require.NoError(t, err) - fakeService.provisioned = map[string][]*models.DashboardProvisioning{ - "Default": { - { - Name: "Default", - ExternalId: absPath, - Updated: stat.ModTime().AddDate(0, 0, -1).Unix(), - CheckSum: checksum, - }, + provisionedDashboard := []*models.DashboardProvisioning{ + { + Name: "Default", + ExternalId: absPath, + Updated: stat.ModTime().AddDate(0, 0, -1).Unix(), + CheckSum: checksum, }, } + fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() + reader, err := NewDashboardFileReader(cfg, logger, nil) + reader.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader.walkDisk(context.Background()) require.NoError(t, err) - require.Equal(t, len(fakeService.inserted), 0) }) t.Run("Dashboard with newer timestamp and different checksum should replace imported dashboard", func(t *testing.T) { @@ -268,36 +255,39 @@ func TestDashboardFileReader(t *testing.T) { stat, err := os.Stat(oneDashboard + "/dashboard1.json") require.NoError(t, err) - fakeService.provisioned = map[string][]*models.DashboardProvisioning{ - "Default": { - { - Name: "Default", - ExternalId: absPath, - Updated: stat.ModTime().AddDate(0, 0, -1).Unix(), - CheckSum: "fakechecksum", - }, + provisionedDashboard := []*models.DashboardProvisioning{ + { + Name: "Default", + ExternalId: absPath, + Updated: stat.ModTime().AddDate(0, 0, -1).Unix(), + CheckSum: "fakechecksum", }, } + fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + reader, err := NewDashboardFileReader(cfg, logger, nil) + reader.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader.walkDisk(context.Background()) require.NoError(t, err) - require.Equal(t, len(fakeService.inserted), 1) }) t.Run("Overrides id from dashboard.json files", func(t *testing.T) { setup() cfg.Options["path"] = containingID + fakeService.On("GetProvisionedDashboardData", configName).Return(nil, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + reader, err := NewDashboardFileReader(cfg, logger, nil) + reader.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader.walkDisk(context.Background()) require.NoError(t, err) - - require.Equal(t, len(fakeService.inserted), 1) }) t.Run("Get folder from files structure", func(t *testing.T) { @@ -305,40 +295,16 @@ func TestDashboardFileReader(t *testing.T) { cfg.Options["path"] = foldersFromFilesStructure cfg.Options["foldersFromFilesStructure"] = true + fakeService.On("GetProvisionedDashboardData", configName).Return(nil, nil).Once() + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(3) + reader, err := NewDashboardFileReader(cfg, logger, nil) + reader.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader.walkDisk(context.Background()) require.NoError(t, err) - - require.Equal(t, len(fakeService.inserted), 5) - - foldersCount := 0 - for _, d := range fakeService.inserted { - if d.Dashboard.IsFolder { - foldersCount++ - } - } - require.Equal(t, foldersCount, 2) - - foldersAndDashboards := make(map[string]struct{}, 5) - for _, d := range fakeService.inserted { - title := d.Dashboard.Title - if _, ok := foldersAndDashboards[title]; ok { - require.Nil(t, fmt.Errorf("dashboard title %q already exists", title)) - } - - switch title { - case "folderOne", "folderTwo": - require.True(t, d.Dashboard.IsFolder) - case "Grafana1", "Grafana2", "RootDashboard": - require.False(t, d.Dashboard.IsFolder) - default: - require.Nil(t, fmt.Errorf("unknown dashboard title %q", title)) - } - - foldersAndDashboards[title] = struct{}{} - } }) t.Run("Invalid configuration should return error", func(t *testing.T) { @@ -367,30 +333,23 @@ func TestDashboardFileReader(t *testing.T) { cfg1 := &config{Name: "1", Type: "file", OrgID: 1, Folder: "f1", Options: map[string]interface{}{"path": containingID}} cfg2 := &config{Name: "2", Type: "file", OrgID: 1, Folder: "f2", Options: map[string]interface{}{"path": containingID}} + fakeService.On("GetProvisionedDashboardData", mock.Anything).Return(nil, nil).Times(2) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) + reader1, err := NewDashboardFileReader(cfg1, logger, nil) + reader1.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader1.walkDisk(context.Background()) require.NoError(t, err) reader2, err := NewDashboardFileReader(cfg2, logger, nil) + reader2.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader2.walkDisk(context.Background()) require.NoError(t, err) - - var folderCount int - var dashCount int - for _, o := range fakeService.inserted { - if o.Dashboard.IsFolder { - folderCount++ - } else { - dashCount++ - } - } - - require.Equal(t, folderCount, 2) - require.Equal(t, dashCount, 2) }) }) @@ -422,16 +381,9 @@ func TestDashboardFileReader(t *testing.T) { }, } - folderID, err := getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + _, err := getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) require.NoError(t, err) - inserted := false - for _, d := range fakeService.inserted { - if d.Dashboard.IsFolder && d.Dashboard.Id == folderID { - inserted = true - } - } - require.Equal(t, len(fakeService.inserted), 1) - require.True(t, inserted) }) t.Run("Walking the folder with dashboards", func(t *testing.T) { @@ -456,56 +408,53 @@ func TestDashboardFileReader(t *testing.T) { absPath2, err := filepath.Abs(unprovision + "/dashboard2.json") require.NoError(t, err) + provisionedDashboard := []*models.DashboardProvisioning{ + {DashboardId: 1, Name: "Default", ExternalId: absPath1}, + {DashboardId: 2, Name: "Default", ExternalId: absPath2}, + } + setupFakeService := func() { setup() cfg = &config{ - Name: "Default", + Name: configName, Type: "file", OrgID: 1, Options: map[string]interface{}{ "folder": unprovision, }, } - - fakeService.inserted = []*dashboards.SaveDashboardDTO{ - {Dashboard: &models.Dashboard{Id: 1}}, - {Dashboard: &models.Dashboard{Id: 2}}, - } - - fakeService.provisioned = map[string][]*models.DashboardProvisioning{ - "Default": { - {DashboardId: 1, Name: "Default", ExternalId: absPath1}, - {DashboardId: 2, Name: "Default", ExternalId: absPath2}, - }, - } } t.Run("Missing dashboard should be unprovisioned if DisableDeletion = true", func(t *testing.T) { setupFakeService() + + fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() + fakeService.On("UnprovisionDashboard", mock.Anything, mock.Anything).Return(nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + cfg.DisableDeletion = true reader, err := NewDashboardFileReader(cfg, logger, nil) + reader.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader.walkDisk(context.Background()) require.NoError(t, err) - - require.Equal(t, len(fakeService.provisioned["Default"]), 1) - require.Equal(t, fakeService.provisioned["Default"][0].ExternalId, absPath1) }) t.Run("Missing dashboard should be deleted if DisableDeletion = false", func(t *testing.T) { setupFakeService() + + fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + fakeService.On("DeleteProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + reader, err := NewDashboardFileReader(cfg, logger, nil) + reader.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader.walkDisk(context.Background()) require.NoError(t, err) - - require.Equal(t, len(fakeService.provisioned["Default"]), 1) - require.Equal(t, fakeService.provisioned["Default"][0].ExternalId, absPath1) - require.Equal(t, len(fakeService.inserted), 1) - require.Equal(t, fakeService.inserted[0].Dashboard.Id, int64(1)) }) }) } @@ -539,111 +488,6 @@ func (ffi FakeFileInfo) Sys() interface{} { return nil } -func mockDashboardProvisioningService() *fakeDashboardProvisioningService { - mock := fakeDashboardProvisioningService{ - provisioned: map[string][]*models.DashboardProvisioning{}, - } - dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService { - return &mock - } - return &mock -} - -type fakeDashboardProvisioningService struct { - dashboards.DashboardProvisioningService - - inserted []*dashboards.SaveDashboardDTO - provisioned map[string][]*models.DashboardProvisioning - getDashboard []*models.Dashboard -} - -func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { - if _, ok := s.provisioned[name]; !ok { - s.provisioned[name] = []*models.DashboardProvisioning{} - } - - return s.provisioned[name], nil -} - -func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO, - provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { - // Copy the structs as we need to change them but do not want to alter outside world. - var copyProvisioning = &models.DashboardProvisioning{} - *copyProvisioning = *provisioning - - var copyDto = &dashboards.SaveDashboardDTO{} - *copyDto = *dto - - if copyDto.Dashboard.Id == 0 { - copyDto.Dashboard.Id = rand.Int63n(1000000) - } else { - err := s.DeleteProvisionedDashboard(context.Background(), dto.Dashboard.Id, dto.Dashboard.OrgId) - // Lets delete existing so we do not have duplicates - if err != nil { - return nil, err - } - } - - s.inserted = append(s.inserted, dto) - - if _, ok := s.provisioned[provisioning.Name]; !ok { - s.provisioned[provisioning.Name] = []*models.DashboardProvisioning{} - } - - for _, val := range s.provisioned[provisioning.Name] { - if val.DashboardId == dto.Dashboard.Id && val.Name == provisioning.Name { - // Do not insert duplicates - return dto.Dashboard, nil - } - } - - copyProvisioning.DashboardId = copyDto.Dashboard.Id - - s.provisioned[provisioning.Name] = append(s.provisioned[provisioning.Name], copyProvisioning) - return dto.Dashboard, nil -} - -func (s *fakeDashboardProvisioningService) SaveFolderForProvisionedDashboards(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) { - s.inserted = append(s.inserted, dto) - return dto.Dashboard, nil -} - -func (s *fakeDashboardProvisioningService) UnprovisionDashboard(ctx context.Context, dashboardID int64) error { - for key, val := range s.provisioned { - for index, dashboard := range val { - if dashboard.DashboardId == dashboardID { - s.provisioned[key] = append(s.provisioned[key][:index], s.provisioned[key][index+1:]...) - } - } - } - return nil -} - -func (s *fakeDashboardProvisioningService) DeleteProvisionedDashboard(ctx context.Context, dashboardID int64, orgID int64) error { - err := s.UnprovisionDashboard(ctx, dashboardID) - if err != nil { - return err - } - - for index, val := range s.inserted { - if val.Dashboard.Id == dashboardID { - s.inserted = append(s.inserted[:index], s.inserted[util.MinInt(index+1, len(s.inserted)):]...) - } - } - return nil -} - -func (s *fakeDashboardProvisioningService) GetProvisionedDashboardDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) { - return nil, nil -} - -func mockGetDashboardQuery(ctx context.Context, cmd *models.GetDashboardQuery) error { - for _, d := range fakeService.getDashboard { - if d.Slug == cmd.Slug { - cmd.Result = d - return nil - } - } - +func mockGetDashboardQuery(_ context.Context, _ *models.GetDashboardQuery) error { return models.ErrDashboardNotFound } diff --git a/pkg/services/provisioning/dashboards/validator_test.go b/pkg/services/provisioning/dashboards/validator_test.go index e1e588e75f9..040e4699b45 100644 --- a/pkg/services/provisioning/dashboards/validator_test.go +++ b/pkg/services/provisioning/dashboards/validator_test.go @@ -5,10 +5,12 @@ import ( "sort" "testing" - "github.com/stretchr/testify/require" - "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/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) const ( @@ -18,7 +20,8 @@ const ( func TestDuplicatesValidator(t *testing.T) { bus.ClearBusHandlers() - fakeService = mockDashboardProvisioningService() + fakeService := &dashboards.FakeDashboardProvisioning{} + defer fakeService.AssertExpectations(t) bus.AddHandler("test", mockGetDashboardQuery) cfg := &config{ @@ -32,6 +35,11 @@ func TestDuplicatesValidator(t *testing.T) { 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) require.NoError(t, err) @@ -47,9 +55,11 @@ func TestDuplicatesValidator(t *testing.T) { } reader1, err := NewDashboardFileReader(cfg1, logger, nil) + reader1.dashboardProvisioningService = fakeService require.NoError(t, err) reader2, err := NewDashboardFileReader(cfg2, logger, nil) + reader2.dashboardProvisioningService = fakeService require.NoError(t, err) duplicateValidator := newDuplicateValidator(logger, []*FileReader{reader1, reader2}) @@ -79,6 +89,11 @@ 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) require.NoError(t, err) @@ -94,9 +109,11 @@ func TestDuplicatesValidator(t *testing.T) { } reader1, err := NewDashboardFileReader(cfg1, logger, nil) + reader1.dashboardProvisioningService = fakeService require.NoError(t, err) reader2, err := NewDashboardFileReader(cfg2, logger, nil) + reader2.dashboardProvisioningService = fakeService require.NoError(t, err) duplicateValidator := newDuplicateValidator(logger, []*FileReader{reader1, reader2}) @@ -135,6 +152,10 @@ 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) + cfg1 := &config{ Name: "first", Type: "file", OrgID: 1, Folder: "duplicates-validator-folder", Options: map[string]interface{}{"path": twoDashboardsWithUID}, @@ -149,12 +170,15 @@ func TestDuplicatesValidator(t *testing.T) { } reader1, err := NewDashboardFileReader(cfg1, logger, nil) + reader1.dashboardProvisioningService = fakeService require.NoError(t, err) reader2, err := NewDashboardFileReader(cfg2, logger, nil) + reader2.dashboardProvisioningService = fakeService require.NoError(t, err) reader3, err := NewDashboardFileReader(cfg3, logger, nil) + reader3.dashboardProvisioningService = fakeService require.NoError(t, err) duplicateValidator := newDuplicateValidator(logger, []*FileReader{reader1, reader2, reader3}) diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index c7210b9279a..1ce04cdb193 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" plugifaces "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" + dashboardservice "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" @@ -20,10 +21,9 @@ import ( ) func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugifaces.Store, - encryptionService encryption.Internal, notificatonService *notifications.NotificationService) (*ProvisioningServiceImpl, error) { + encryptionService encryption.Internal, notificatonService *notifications.NotificationService, dashboardsStore dashboardservice.Store) (*ProvisioningServiceImpl, error) { s := &ProvisioningServiceImpl{ Cfg: cfg, - SQLStore: sqlStore, pluginStore: pluginStore, EncryptionService: encryptionService, NotificationService: notificatonService, @@ -32,6 +32,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p provisionNotifiers: notifiers.Provision, provisionDatasources: datasources.Provision, provisionPlugins: plugins.Provision, + dashboardsStore: dashboardsStore, } return s, nil } @@ -88,6 +89,7 @@ type ProvisioningServiceImpl struct { provisionDatasources func(context.Context, string) error provisionPlugins func(context.Context, string, plugifaces.Store) error mutex sync.Mutex + dashboardsStore dashboardservice.Store } func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error { @@ -170,7 +172,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.SQLStore) + dashProvisioner, err := ps.newDashboardProvisioner(ctx, dashboardPath, ps.dashboardsStore) 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 731b3c0a3ce..8e9253b76d4 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - dboards "github.com/grafana/grafana/pkg/dashboards" + dashboardstore "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" @@ -92,7 +92,7 @@ func setup() *serviceTestStruct { } serviceTest.service = newProvisioningServiceImpl( - func(context.Context, string, dboards.Store) (dashboards.DashboardProvisioner, error) { + func(context.Context, string, dashboardstore.Store) (dashboards.DashboardProvisioner, error) { return serviceTest.mock, nil }, nil, diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index d7a07a72eec..a593fbb9d6c 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -4,18 +4,14 @@ import ( "context" "fmt" "strings" - "time" - - "github.com/prometheus/client_golang/prometheus" - - "github.com/grafana/grafana/pkg/services/sqlstore/permissions" - "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/services/sqlstore/permissions" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/util" + "github.com/prometheus/client_golang/prometheus" ) var shadowSearchCounter = prometheus.NewCounterVec( @@ -47,166 +43,6 @@ func (ss *SQLStore) addDashboardQueryAndCommandHandlers() { var generateNewUid func() string = util.GenerateShortUID -func (ss *SQLStore) SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) { - err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { - return saveDashboard(sess, &cmd) - }) - return cmd.Result, err -} - -func saveDashboard(sess *DBSession, cmd *models.SaveDashboardCommand) error { - dash := cmd.GetDashboardModel() - - userId := cmd.UserId - - if userId == 0 { - userId = -1 - } - - if dash.Id > 0 { - var existing models.Dashboard - dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing) - if err != nil { - return err - } - if !dashWithIdExists { - return models.ErrDashboardNotFound - } - - // check for is someone else has written in between - if dash.Version != existing.Version { - if cmd.Overwrite { - dash.SetVersion(existing.Version) - } else { - return models.ErrDashboardVersionMismatch - } - } - - // do not allow plugin dashboard updates without overwrite flag - if existing.PluginId != "" && !cmd.Overwrite { - return models.UpdatePluginDashboardError{PluginId: existing.PluginId} - } - } - - if dash.Uid == "" { - uid, err := generateNewDashboardUid(sess, dash.OrgId) - if err != nil { - return err - } - dash.SetUid(uid) - } - - parentVersion := dash.Version - var affectedRows int64 - var err error - - if dash.Id == 0 { - dash.SetVersion(1) - dash.Created = time.Now() - dash.CreatedBy = userId - dash.Updated = time.Now() - dash.UpdatedBy = userId - metrics.MApiDashboardInsert.Inc() - affectedRows, err = sess.Insert(dash) - } else { - dash.SetVersion(dash.Version + 1) - - if !cmd.UpdatedAt.IsZero() { - dash.Updated = cmd.UpdatedAt - } else { - dash.Updated = time.Now() - } - - dash.UpdatedBy = userId - - affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash) - } - - if err != nil { - return err - } - - if affectedRows == 0 { - return models.ErrDashboardNotFound - } - - dashVersion := &models.DashboardVersion{ - DashboardId: dash.Id, - ParentVersion: parentVersion, - RestoredFrom: cmd.RestoredFrom, - Version: dash.Version, - Created: time.Now(), - CreatedBy: dash.UpdatedBy, - Message: cmd.Message, - Data: dash.Data, - } - - // insert version entry - if affectedRows, err = sess.Insert(dashVersion); err != nil { - return err - } else if affectedRows == 0 { - return models.ErrDashboardNotFound - } - - // delete existing tags - _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id) - if err != nil { - return err - } - - // insert new tags - tags := dash.GetTags() - if len(tags) > 0 { - for _, tag := range tags { - if _, err := sess.Insert(&DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil { - return err - } - } - } - - cmd.Result = dash - - return nil -} - -func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) { - for i := 0; i < 3; i++ { - uid := generateNewUid() - - exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&models.Dashboard{}) - if err != nil { - return "", err - } - - if !exists { - return uid, nil - } - } - - return "", models.ErrDashboardFailedGenerateUniqueUid -} - -// GetDashboardByTitle gets a dashboard by its title. -func (ss *SQLStore) GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error) { - if title == "" { - return nil, models.ErrDashboardIdentifierNotSet - } - - // there is a unique constraint on org_id, folder_id, title - // there are no nested folders so the parent folder id is always 0 - dashboard := models.Dashboard{OrgId: orgID, FolderId: 0, Title: title} - has, err := ss.engine.Table(&models.Dashboard{}).Where("is_folder = " + dialect.BooleanStr(true)).Where("folder_id=0").Get(&dashboard) - if err != nil { - return nil, err - } - if !has { - return nil, models.ErrDashboardNotFound - } - dashboard.SetId(dashboard.Id) - dashboard.SetUid(dashboard.Uid) - return &dashboard, nil -} - func (ss *SQLStore) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error { return withDbSession(ctx, x, func(dbSession *DBSession) error { if query.Id == 0 && len(query.Slug) == 0 && len(query.Uid) == 0 { @@ -243,7 +79,7 @@ type DashboardSearchProjection struct { SortMeta int64 } -func (ss *SQLStore) findDashboards(ctx context.Context, query *search.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { +func (ss *SQLStore) FindDashboards(ctx context.Context, query *search.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { filters := []interface{}{ permissions.DashboardPermissionFilter{ OrgRole: query.SignedInUser.OrgRole, @@ -315,7 +151,7 @@ func (ss *SQLStore) findDashboards(ctx context.Context, query *search.FindPersis } func (ss *SQLStore) SearchDashboards(ctx context.Context, query *search.FindPersistedDashboardsQuery) error { - res, err := ss.findDashboards(ctx, query) + res, err := ss.FindDashboards(ctx, query) if err != nil { return err } @@ -619,155 +455,6 @@ func (ss *SQLStore) GetDashboardUIDById(ctx context.Context, query *models.GetDa }) } -func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, dash *models.Dashboard, overwrite bool) (bool, error) { - dashWithIdExists := false - isParentFolderChanged := false - var existingById models.Dashboard - - if dash.Id > 0 { - var err error - dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById) - if err != nil { - return isParentFolderChanged, fmt.Errorf("SQL query for existing dashboard by ID failed: %w", err) - } - - if !dashWithIdExists { - return isParentFolderChanged, models.ErrDashboardNotFound - } - - if dash.Uid == "" { - dash.SetUid(existingById.Uid) - } - } - - dashWithUidExists := false - var existingByUid models.Dashboard - - if dash.Uid != "" { - var err error - dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid) - if err != nil { - return isParentFolderChanged, fmt.Errorf("SQL query for existing dashboard by UID failed: %w", err) - } - } - - if dash.FolderId > 0 { - var existingFolder models.Dashboard - folderExists, err := sess.Where("org_id=? AND id=? AND is_folder=?", dash.OrgId, dash.FolderId, - dialect.BooleanStr(true)).Get(&existingFolder) - if err != nil { - return isParentFolderChanged, fmt.Errorf("SQL query for folder failed: %w", err) - } - - if !folderExists { - return isParentFolderChanged, models.ErrDashboardFolderNotFound - } - } - - if !dashWithIdExists && !dashWithUidExists { - return isParentFolderChanged, nil - } - - if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id { - return isParentFolderChanged, models.ErrDashboardWithSameUIDExists - } - - existing := existingById - - if !dashWithIdExists && dashWithUidExists { - dash.SetId(existingByUid.Id) - dash.SetUid(existingByUid.Uid) - existing = existingByUid - - if !dash.IsFolder { - isParentFolderChanged = true - } - } - - if (existing.IsFolder && !dash.IsFolder) || - (!existing.IsFolder && dash.IsFolder) { - return isParentFolderChanged, models.ErrDashboardTypeMismatch - } - - if !dash.IsFolder && dash.FolderId != existing.FolderId { - isParentFolderChanged = true - } - - // check for is someone else has written in between - if dash.Version != existing.Version { - if overwrite { - dash.SetVersion(existing.Version) - } else { - return isParentFolderChanged, models.ErrDashboardVersionMismatch - } - } - - // do not allow plugin dashboard updates without overwrite flag - if existing.PluginId != "" && !overwrite { - return isParentFolderChanged, models.UpdatePluginDashboardError{PluginId: existing.PluginId} - } - - return isParentFolderChanged, nil -} - -func getExistingDashboardByTitleAndFolder(sess *DBSession, dash *models.Dashboard, overwrite, - isParentFolderChanged bool) (bool, error) { - var existing models.Dashboard - exists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, - dialect.BooleanStr(true), dash.FolderId).Get(&existing) - if err != nil { - return isParentFolderChanged, fmt.Errorf("SQL query for existing dashboard by org ID or folder ID failed: %w", err) - } - - if exists && dash.Id != existing.Id { - if existing.IsFolder && !dash.IsFolder { - return isParentFolderChanged, models.ErrDashboardWithSameNameAsFolder - } - - if !existing.IsFolder && dash.IsFolder { - return isParentFolderChanged, models.ErrDashboardFolderWithSameNameAsDashboard - } - - if !dash.IsFolder && (dash.FolderId != existing.FolderId || dash.Id == 0) { - isParentFolderChanged = true - } - - if overwrite { - dash.SetId(existing.Id) - dash.SetUid(existing.Uid) - dash.SetVersion(existing.Version) - } else { - return isParentFolderChanged, models.ErrDashboardWithSameNameInFolderExists - } - } - - return isParentFolderChanged, nil -} - -func (ss *SQLStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) { - isParentFolderChanged := false - err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { - var err error - isParentFolderChanged, err = getExistingDashboardByIdOrUidForUpdate(sess, dashboard, overwrite) - if err != nil { - return err - } - - isParentFolderChanged, err = getExistingDashboardByTitleAndFolder(sess, dashboard, overwrite, - isParentFolderChanged) - if err != nil { - return err - } - - return nil - }) - if err != nil { - return false, err - } - - return isParentFolderChanged, nil -} - // HasEditPermissionInFolders validates that an user have access to a certain folder func (ss *SQLStore) HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { return withDbSession(ctx, x, func(dbSession *DBSession) error { diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go index 15a032cdecb..bcc04b2a17a 100644 --- a/pkg/services/sqlstore/dashboard_acl.go +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -2,7 +2,6 @@ package sqlstore import ( "context" - "fmt" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" @@ -12,40 +11,6 @@ func (ss *SQLStore) addDashboardACLQueryAndCommandHandlers() { bus.AddHandler("sql", ss.GetDashboardAclInfoList) } -func (ss *SQLStore) UpdateDashboardACL(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error { - return ss.UpdateDashboardACLCtx(ctx, dashboardID, items) -} - -func (ss *SQLStore) UpdateDashboardACLCtx(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error { - return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { - // delete existing items - _, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", dashboardID) - if err != nil { - return fmt.Errorf("deleting from dashboard_acl failed: %w", err) - } - - for _, item := range items { - if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { - return models.ErrDashboardAclInfoMissing - } - - if item.DashboardID == 0 { - return models.ErrDashboardPermissionDashboardEmpty - } - - sess.Nullable("user_id", "team_id") - if _, err := sess.Insert(item); err != nil { - return err - } - } - - // Update dashboard HasAcl flag - dashboard := models.Dashboard{HasAcl: true} - _, err = sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard) - return err - }) -} - // GetDashboardAclInfoList returns a list of permissions for a dashboard. They can be fetched from three // different places. // 1) Permissions for the dashboard diff --git a/pkg/services/sqlstore/dashboard_provisioning.go b/pkg/services/sqlstore/dashboard_provisioning.go index 053f5ad74c8..cc46473caa3 100644 --- a/pkg/services/sqlstore/dashboard_provisioning.go +++ b/pkg/services/sqlstore/dashboard_provisioning.go @@ -9,7 +9,6 @@ import ( ) func (ss *SQLStore) addDashboardProvisioningQueryAndCommandHandlers() { - bus.AddHandler("sql", UnprovisionDashboard) bus.AddHandler("sql", ss.DeleteOrphanedProvisionedDashboards) } @@ -20,97 +19,6 @@ type DashboardExtras struct { Value string } -func (ss *SQLStore) GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) { - var data models.DashboardProvisioning - exists, err := x.Where("dashboard_id = ?", dashboardID).Get(&data) - if err != nil { - return nil, err - } - if exists { - return &data, nil - } - return nil, nil -} - -func (ss *SQLStore) GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) { - var provisionedDashboard models.DashboardProvisioning - err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { - var dashboard models.Dashboard - exists, err := sess.Where("org_id = ? AND uid = ?", orgID, dashboardUID).Get(&dashboard) - if err != nil { - return err - } - if !exists { - return models. - ErrDashboardNotFound - } - exists, err = sess.Where("dashboard_id = ?", dashboard.Id).Get(&provisionedDashboard) - if err != nil { - return err - } - if !exists { - return models.ErrProvisionedDashboardNotFound - } - return nil - }) - return &provisionedDashboard, err -} - -func (ss *SQLStore) SaveProvisionedDashboard(cmd models.SaveDashboardCommand, - provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { - err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { - if err := saveDashboard(sess, &cmd); err != nil { - return err - } - - if provisioning.Updated == 0 { - provisioning.Updated = cmd.Result.Updated.Unix() - } - - return saveProvisionedData(sess, provisioning, cmd.Result) - }) - - return cmd.Result, err -} - -func saveProvisionedData(sess *DBSession, provisioning *models.DashboardProvisioning, dashboard *models.Dashboard) error { - result := &models.DashboardProvisioning{} - - exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, provisioning.Name).Get(result) - if err != nil { - return err - } - - provisioning.Id = result.Id - provisioning.DashboardId = dashboard.Id - - if exist { - _, err = sess.ID(result.Id).Update(provisioning) - } else { - _, err = sess.Insert(provisioning) - } - - return err -} - -func (ss *SQLStore) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { - var result []*models.DashboardProvisioning - if err := ss.engine.Where("name = ?", name).Find(&result); err != nil { - return nil, err - } - - return result, nil -} - -// UnprovisionDashboard removes row in dashboard_provisioning for the dashboard making it seem as if manually created. -// The dashboard will still have `created_by = -1` to see it was not created by any particular user. -func UnprovisionDashboard(ctx context.Context, cmd *models.UnprovisionDashboardCommand) error { - if _, err := x.Where("dashboard_id = ?", cmd.Id).Delete(&models.DashboardProvisioning{}); err != nil { - return err - } - return nil -} - func (ss *SQLStore) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error { var result []*models.DashboardProvisioning diff --git a/pkg/services/sqlstore/dashboard_version_test.go b/pkg/services/sqlstore/dashboard_version_test.go index 6b9536d1d08..07adedc92ab 100644 --- a/pkg/services/sqlstore/dashboard_version_test.go +++ b/pkg/services/sqlstore/dashboard_version_test.go @@ -7,10 +7,12 @@ import ( "context" "reflect" "testing" + "time" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" "github.com/stretchr/testify/require" ) @@ -19,13 +21,60 @@ func updateTestDashboard(t *testing.T, sqlStore *SQLStore, dashboard *models.Das data["id"] = dashboard.Id - saveCmd := models.SaveDashboardCommand{ + parentVersion := dashboard.Version + + cmd := models.SaveDashboardCommand{ OrgId: dashboard.OrgId, Overwrite: true, Dashboard: simplejson.NewFromAny(data), } - _, err := sqlStore.SaveDashboard(saveCmd) + var dash *models.Dashboard + err := sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { + var existing models.Dashboard + dash = cmd.GetDashboardModel() + dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing) + require.NoError(t, err) + require.True(t, dashWithIdExists) + + if dash.Version != existing.Version { + dash.SetVersion(existing.Version) + dash.Version = existing.Version + } + + dash.SetVersion(dash.Version + 1) + dash.Created = time.Now() + dash.Updated = time.Now() + dash.Id = dashboard.Id + dash.Uid = util.GenerateShortUID() + + _, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash) + return err + }) + require.Nil(t, err) + + err = sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { + dashVersion := &models.DashboardVersion{ + DashboardId: dash.Id, + ParentVersion: parentVersion, + RestoredFrom: cmd.RestoredFrom, + Version: dash.Version, + Created: time.Now(), + CreatedBy: dash.UpdatedBy, + Message: cmd.Message, + Data: dash.Data, + } + + if affectedRows, err := sess.Insert(dashVersion); err != nil { + return err + } else if affectedRows == 0 { + return models.ErrDashboardNotFound + } + + return nil + }) + + require.NoError(t, err) } func TestGetDashboardVersion(t *testing.T) { @@ -94,7 +143,6 @@ func TestGetDashboardVersions(t *testing.T) { updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{ "tags": "different-tag", }) - query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1} err := sqlStore.GetDashboardVersions(context.Background(), &query) diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go index b9ee9aaf9ea..889e2d24fb0 100644 --- a/pkg/services/sqlstore/mockstore/mockstore.go +++ b/pkg/services/sqlstore/mockstore/mockstore.go @@ -112,23 +112,7 @@ func (m *SQLStoreMock) DeleteOrg(ctx context.Context, cmd *models.DeleteOrgComma return m.ExpectedError } -func (m *SQLStoreMock) GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) { - return &models.DashboardProvisioning{}, m.ExpectedError -} - -func (m *SQLStoreMock) GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) { - return nil, m.ExpectedError -} - -func (m *SQLStoreMock) SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { - return nil, m.ExpectedError -} - -func (m *SQLStoreMock) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { - return nil, m.ExpectedError -} - -func (m *SQLStoreMock) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error { +func (m SQLStoreMock) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error { return m.ExpectedError } @@ -382,16 +366,7 @@ func (m *SQLStoreMock) DeleteExpiredVersions(ctx context.Context, cmd *models.De return m.ExpectedError } -func (m *SQLStoreMock) UpdateDashboardACL(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error { - return m.ExpectedError -} - -func (m *SQLStoreMock) UpdateDashboardACLCtx(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error { - return m.ExpectedError -} - -func (m *SQLStoreMock) GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error { - query.Result = m.ExpectedDashboardAclInfoList +func (m SQLStoreMock) GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error { return m.ExpectedError } @@ -433,11 +408,7 @@ func (m *SQLStoreMock) HandleAlertsQuery(ctx context.Context, query *models.GetA return m.ExpectedError } -func (m *SQLStoreMock) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error { - return m.ExpectedError -} - -func (m *SQLStoreMock) SetAlertState(ctx context.Context, cmd *models.SetAlertStateCommand) error { +func (m SQLStoreMock) SetAlertState(ctx context.Context, cmd *models.SetAlertStateCommand) error { return m.ExpectedError } @@ -484,18 +455,14 @@ func (m *SQLStoreMock) GetDashboard(ctx context.Context, query *models.GetDashbo return m.ExpectedError } +func (m SQLStoreMock) SearchDashboards(ctx context.Context, query *search.FindPersistedDashboardsQuery) error { + return m.ExpectedError +} + func (m *SQLStoreMock) GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error { return nil // TODO: Implement } -func (m *SQLStoreMock) GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error) { - return nil, m.ExpectedError -} - -func (m *SQLStoreMock) SearchDashboards(ctx context.Context, query *search.FindPersistedDashboardsQuery) error { - return m.ExpectedError -} - func (m *SQLStoreMock) DeleteDashboard(ctx context.Context, cmd *models.DeleteDashboardCommand) error { cmd.Id = m.ExpectedDashboard.Id cmd.OrgId = m.ExpectedDashboard.OrgId @@ -510,11 +477,7 @@ func (m *SQLStoreMock) GetDashboardUIDById(ctx context.Context, query *models.Ge return m.ExpectedError } -func (m *SQLStoreMock) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) { - return false, nil -} - -func (m *SQLStoreMock) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error { +func (m SQLStoreMock) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error { query.Result = m.ExpectedDatasource return m.ExpectedError } diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index 95f3056e35f..10b46601ecb 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -9,8 +9,10 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" "github.com/stretchr/testify/require" ) @@ -329,12 +331,12 @@ func TestAccountDataAccess(t *testing.T) { dash1 := insertTestDashboard(t, sqlStore, "1 test dash", ac1.OrgId, 0, false, "prod", "webapp") dash2 := insertTestDashboard(t, sqlStore, "2 test dash", ac3.OrgId, 0, false, "prod", "webapp") - err = testHelperUpdateDashboardAcl(t, sqlStore, dash1.Id, models.DashboardAcl{ + err = updateDashboardAcl(t, sqlStore, dash1.Id, &models.DashboardAcl{ DashboardID: dash1.Id, OrgID: ac1.OrgId, UserID: ac3.Id, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) - err = testHelperUpdateDashboardAcl(t, sqlStore, dash2.Id, models.DashboardAcl{ + err = updateDashboardAcl(t, sqlStore, dash2.Id, &models.DashboardAcl{ DashboardID: dash2.Id, OrgID: ac3.OrgId, UserID: ac3.Id, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -370,16 +372,92 @@ func TestAccountDataAccess(t *testing.T) { }) } -func testHelperUpdateDashboardAcl(t *testing.T, sqlStore *SQLStore, dashboardID int64, - items ...models.DashboardAcl) error { +//TODO: Use FakeDashboardStore when org has its own service +func insertTestDashboard(t *testing.T, sqlStore *SQLStore, title string, orgId int64, + folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { + t.Helper() + cmd := models.SaveDashboardCommand{ + OrgId: orgId, + FolderId: folderId, + IsFolder: isFolder, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": title, + "tags": tags, + }), + } + + var dash *models.Dashboard + err := sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { + dash = cmd.GetDashboardModel() + dash.SetVersion(1) + dash.Created = time.Now() + dash.Updated = time.Now() + dash.Uid = util.GenerateShortUID() + _, err := sess.Insert(dash) + return err + }) + + require.NoError(t, err) + require.NotNil(t, dash) + dash.Data.Set("id", dash.Id) + dash.Data.Set("uid", dash.Uid) + + err = sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { + dashVersion := &models.DashboardVersion{ + DashboardId: dash.Id, + ParentVersion: dash.Version, + RestoredFrom: cmd.RestoredFrom, + Version: dash.Version, + Created: time.Now(), + CreatedBy: dash.UpdatedBy, + Message: cmd.Message, + Data: dash.Data, + } + + if affectedRows, err := sess.Insert(dashVersion); err != nil { + return err + } else if affectedRows == 0 { + return models.ErrDashboardNotFound + } + + return nil + }) + + return dash +} + +//TODO: Use FakeDashboardStore when org has its own service +func updateDashboardAcl(t *testing.T, sqlStore *SQLStore, dashboardID int64, items ...*models.DashboardAcl) error { t.Helper() - var itemPtrs []*models.DashboardAcl - for _, it := range items { - item := it - item.Created = time.Now() - item.Updated = time.Now() - itemPtrs = append(itemPtrs, &item) - } - return sqlStore.UpdateDashboardACL(context.Background(), dashboardID, itemPtrs) + err := sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { + _, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", dashboardID) + if err != nil { + return fmt.Errorf("deleting from dashboard_acl failed: %w", err) + } + + for _, item := range items { + item.Created = time.Now() + item.Updated = time.Now() + if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { + return models.ErrDashboardAclInfoMissing + } + + if item.DashboardID == 0 { + return models.ErrDashboardPermissionDashboardEmpty + } + + sess.Nullable("user_id", "team_id") + if _, err := sess.Insert(item); err != nil { + return err + } + } + + // Update dashboard HasAcl flag + dashboard := models.Dashboard{HasAcl: true} + _, err = sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard) + return err + }) + return err } diff --git a/pkg/services/sqlstore/searchstore/search_test.go b/pkg/services/sqlstore/searchstore/search_test.go index f66cc1d373f..1d17b593d50 100644 --- a/pkg/services/sqlstore/searchstore/search_test.go +++ b/pkg/services/sqlstore/searchstore/search_test.go @@ -1,19 +1,16 @@ -//go:build integration -// +build integration - // package search_test contains integration tests for search package searchstore_test import ( "context" "testing" - "time" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" + "github.com/grafana/grafana/pkg/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -171,11 +168,27 @@ func createDashboards(t *testing.T, db *sqlstore.SQLStore, startID, endID int, o "version": 0 }`)) require.NoError(t, err) - dash, err := db.SaveDashboard(models.SaveDashboardCommand{ - Dashboard: dashboard, - UserId: 1, - OrgId: orgID, - UpdatedAt: time.Now(), + + var dash *models.Dashboard + err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + dash = models.NewDashboardFromJson(dashboard) + dash.OrgId = orgID + dash.Uid = util.GenerateShortUID() + dash.CreatedBy = 1 + dash.UpdatedBy = 1 + _, err := sess.Insert(dash) + require.NoError(t, err) + + tags := dash.GetTags() + if len(tags) > 0 { + for _, tag := range tags { + if _, err := sess.Insert(&sqlstore.DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil { + return err + } + } + } + + return nil }) require.NoError(t, err) diff --git a/pkg/services/sqlstore/sqlbuilder_test.go b/pkg/services/sqlstore/sqlbuilder_test.go index 7ef90102343..1b3065860f6 100644 --- a/pkg/services/sqlstore/sqlbuilder_test.go +++ b/pkg/services/sqlstore/sqlbuilder_test.go @@ -244,7 +244,7 @@ func createDummyDashboard(t *testing.T, sqlStore *SQLStore, dashboardProps Dashb saveDashboardCmd.OrgId = 1 } - dash, err := sqlStore.SaveDashboard(saveDashboardCmd) + dash := insertTestDashboard(t, sqlStore, "", saveDashboardCmd.OrgId, 0, false, nil) require.NoError(t, err) t.Logf("Created dashboard with ID %d and org ID %d\n", dash.Id, dash.OrgId) @@ -287,7 +287,7 @@ func createDummyACL(t *testing.T, sqlStore *SQLStore, dashboardPermission *Dashb acl.Role = &dashboardPermission.Role } - err := sqlStore.UpdateDashboardACL(context.Background(), dashboardID, []*models.DashboardAcl{acl}) + err := updateDashboardAcl(t, sqlStore, dashboardID, acl) require.NoError(t, err) if user != nil { return user.Id diff --git a/pkg/services/sqlstore/store.go b/pkg/services/sqlstore/store.go index d97b593d1d9..a5e96afbcb8 100644 --- a/pkg/services/sqlstore/store.go +++ b/pkg/services/sqlstore/store.go @@ -24,10 +24,6 @@ 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 - GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) - GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) - SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) - GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error CreateLoginAttempt(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error DeleteOldLoginAttempts(ctx context.Context, cmd *models.DeleteOldLoginAttemptsCommand) error @@ -87,8 +83,6 @@ type Store interface { GetDashboardVersion(ctx context.Context, query *models.GetDashboardVersionQuery) error GetDashboardVersions(ctx context.Context, query *models.GetDashboardVersionsQuery) error DeleteExpiredVersions(ctx context.Context, cmd *models.DeleteExpiredVersionsCommand) error - UpdateDashboardACL(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error - UpdateDashboardACLCtx(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error CreatePlaylist(ctx context.Context, cmd *models.CreatePlaylistCommand) error UpdatePlaylist(ctx context.Context, cmd *models.UpdatePlaylistCommand) error @@ -99,7 +93,6 @@ type Store interface { GetAlertById(ctx context.Context, query *models.GetAlertByIdQuery) error GetAllAlertQueryHandler(ctx context.Context, query *models.GetAllAlertsQuery) error HandleAlertsQuery(ctx context.Context, query *models.GetAlertsQuery) error - SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error SetAlertState(ctx context.Context, cmd *models.SetAlertStateCommand) error PauseAlert(ctx context.Context, cmd *models.PauseAlertCommand) error PauseAllAlerts(ctx context.Context, cmd *models.PauseAllAlertCommand) error @@ -109,15 +102,12 @@ type Store interface { GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQuery) error SearchOrgUsers(ctx context.Context, query *models.SearchOrgUsersQuery) error RemoveOrgUser(ctx context.Context, cmd *models.RemoveOrgUserCommand) error - SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error - GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error) SearchDashboards(ctx context.Context, query *search.FindPersistedDashboardsQuery) error DeleteDashboard(ctx context.Context, cmd *models.DeleteDashboardCommand) error GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error - ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error GetDataSources(ctx context.Context, query *models.GetDataSourcesQuery) error GetDataSourcesByType(ctx context.Context, query *models.GetDataSourcesByTypeQuery) error diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index ecc260ae82a..97d41c79392 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -274,7 +274,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { require.NoError(t, err) err = sqlStore.AddTeamMember(userIds[2], testOrgID, groupId, false, 0) require.NoError(t, err) - err = testHelperUpdateDashboardAcl(t, sqlStore, 1, models.DashboardAcl{ + err = updateDashboardAcl(t, sqlStore, 1, &models.DashboardAcl{ DashboardID: 1, OrgID: testOrgID, Permission: models.PERMISSION_EDIT, TeamID: groupId, }) require.NoError(t, err) diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index 84f787afbbc..1a50fe1ccf6 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -239,7 +239,7 @@ func TestUserDataAccess(t *testing.T) { }) require.Nil(t, err) - err = testHelperUpdateDashboardAcl(t, ss, 1, models.DashboardAcl{ + err = updateDashboardAcl(t, ss, 1, &models.DashboardAcl{ DashboardID: 1, OrgID: users[0].OrgId, UserID: users[1].Id, Permission: models.PERMISSION_EDIT, }) @@ -290,7 +290,7 @@ func TestUserDataAccess(t *testing.T) { }) require.Nil(t, err) - err = testHelperUpdateDashboardAcl(t, ss, 1, models.DashboardAcl{ + err = updateDashboardAcl(t, ss, 1, &models.DashboardAcl{ DashboardID: 1, OrgID: users[0].OrgId, UserID: users[1].Id, Permission: models.PERMISSION_EDIT, }) diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index bd2fc42e7a0..5a0d21fe0a6 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -12,15 +12,15 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards/database" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" @@ -2631,7 +2631,8 @@ func createFolder(t *testing.T, store *sqlstore.SQLStore, folderID int64, folder "title": folderName, }), } - f, err := store.SaveDashboard(cmd) + dashboardsStore := database.ProvideDashboardStore(store) + f, err := dashboardsStore.SaveDashboard(cmd) if err != nil { return "", err diff --git a/pkg/tests/api/alerting/api_prometheus_test.go b/pkg/tests/api/alerting/api_prometheus_test.go index ccaa162b0af..011443c546b 100644 --- a/pkg/tests/api/alerting/api_prometheus_test.go +++ b/pkg/tests/api/alerting/api_prometheus_test.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" + dashboardsstore "github.com/grafana/grafana/pkg/services/dashboards/database" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/tests/testinfra" @@ -634,6 +635,7 @@ func TestPrometheusRulesPermissions(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) + dashboardsStore := dashboardsstore.ProvideDashboardStore(store) // override bus to get the GetSignedInUserQuery handler store.Bus = bus.GetBus() @@ -729,7 +731,7 @@ func TestPrometheusRulesPermissions(t *testing.T) { } // remove permissions from folder2 - require.NoError(t, store.UpdateDashboardACL(context.Background(), 2, nil)) + require.NoError(t, dashboardsStore.UpdateDashboardACL(context.Background(), 2, nil)) // make sure that folder2 is not included in the response { @@ -778,7 +780,7 @@ func TestPrometheusRulesPermissions(t *testing.T) { } // remove permissions from _ALL_ folders - require.NoError(t, store.UpdateDashboardACL(context.Background(), 1, nil)) + require.NoError(t, dashboardsStore.UpdateDashboardACL(context.Background(), 1, nil)) // make sure that no folders are included in the response { diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index a90dcdaabca..c3576ac77c6 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" + dashboardsstore "github.com/grafana/grafana/pkg/services/dashboards/database" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/tests/testinfra" @@ -34,6 +35,7 @@ func TestAlertRulePermissions(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) + dashboardsStore := dashboardsstore.ProvideDashboardStore(store) // override bus to get the GetSignedInUserQuery handler store.Bus = bus.GetBus() @@ -180,7 +182,7 @@ func TestAlertRulePermissions(t *testing.T) { assert.JSONEq(t, expectedGetNamespaceResponseBody, body) // remove permissions from folder2 - require.NoError(t, store.UpdateDashboardACL(context.Background(), 2, nil)) + require.NoError(t, dashboardsStore.UpdateDashboardACL(context.Background(), 2, nil)) // make sure that folder2 is not included in the response // nolint:gosec @@ -253,7 +255,7 @@ func TestAlertRulePermissions(t *testing.T) { } // Remove permissions from ALL folders. - require.NoError(t, store.UpdateDashboardACL(context.Background(), 1, nil)) + require.NoError(t, dashboardsStore.UpdateDashboardACL(context.Background(), 1, nil)) { u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules", grafanaListedAddr) // nolint:gosec