From eb82a756685edcb1843b41d853d9b2928b0c5628 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Tue, 30 Apr 2019 13:32:18 +0200 Subject: [PATCH] Provisioning: Show file path of provisioning file in save/delete dialogs (#16706) * Add file path to metadata and show it in dialogs * Make path relative to config directory * Fix tests * Add test for the relative path * Refactor to use path relative to provisioner path * Change return types * Rename attribute * Small fixes from review --- pkg/api/api.go | 4 +- pkg/api/dashboard.go | 17 +++-- pkg/api/dashboard_test.go | 69 ++++++++++++++----- pkg/api/dtos/dashboard.go | 45 ++++++------ pkg/api/http_server.go | 32 +++++---- pkg/models/dashboards.go | 8 +-- pkg/services/dashboards/dashboard_service.go | 26 ++++--- .../dashboards/dashboard_service_test.go | 28 ++++---- .../dashboards/folder_service_test.go | 3 +- .../provisioning/dashboards/dashboard.go | 18 +++-- .../provisioning/dashboards/dashboard_mock.go | 21 ++++-- .../provisioning/dashboards/file_reader.go | 15 ++-- .../dashboards/file_reader_linux_test.go | 2 +- .../dashboards/file_reader_test.go | 6 +- pkg/services/provisioning/provisioning.go | 26 ++++--- .../provisioning/provisioning_mock.go | 58 ++++++++++++++++ .../provisioning/provisioning_test.go | 2 +- .../sqlstore/dashboard_provisioning.go | 8 +-- .../sqlstore/dashboard_provisioning_test.go | 16 ++--- .../dashboard_service_integration_test.go | 4 +- .../DashboardSettings/SettingsCtrl.ts | 2 + .../SaveProvisionedDashboardModalCtrl.ts | 8 ++- public/app/types/dashboard.ts | 1 + 23 files changed, 285 insertions(+), 134 deletions(-) create mode 100644 pkg/services/provisioning/provisioning_mock.go diff --git a/pkg/api/api.go b/pkg/api/api.go index a727ed15e96..df532442f86 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -283,10 +283,10 @@ func (hs *HTTPServer) registerRoutes() { // Dashboard apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) { - dashboardRoute.Get("/uid/:uid", Wrap(GetDashboard)) + dashboardRoute.Get("/uid/:uid", Wrap(hs.GetDashboard)) dashboardRoute.Delete("/uid/:uid", Wrap(DeleteDashboardByUID)) - dashboardRoute.Get("/db/:slug", Wrap(GetDashboard)) + dashboardRoute.Get("/db/:slug", Wrap(hs.GetDashboard)) dashboardRoute.Delete("/db/:slug", Wrap(DeleteDashboardBySlug)) dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index a9d78383045..212b5684280 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path" + "path/filepath" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/dashboards" @@ -47,7 +48,7 @@ func dashboardGuardianResponse(err error) Response { return Error(403, "Access denied to this dashboard", nil) } -func GetDashboard(c *m.ReqContext) Response { +func (hs *HTTPServer) GetDashboard(c *m.ReqContext) Response { dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid")) if rsp != nil { return rsp @@ -106,14 +107,22 @@ func GetDashboard(c *m.ReqContext) Response { meta.FolderUrl = query.Result.GetUrl() } - isDashboardProvisioned := &m.IsDashboardProvisionedQuery{DashboardId: dash.Id} - err = bus.Dispatch(isDashboardProvisioned) + provisioningData, err := dashboards.NewProvisioningService().GetProvisionedDashboardDataByDashboardId(dash.Id) if err != nil { return Error(500, "Error while checking if dashboard is provisioned", err) } - if isDashboardProvisioned.Result { + if provisioningData != nil { meta.Provisioned = true + meta.ProvisionedExternalId, err = filepath.Rel( + hs.ProvisioningService.GetDashboardProvisionerResolvedPath(provisioningData.Name), + provisioningData.ExternalId, + ) + if err != nil { + // Not sure when this could happen so not sure how to better handle this. Right now ProvisionedExternalId + // is for better UX, showing in Save/Delete dialogs and so it won't break anything if it is empty. + hs.log.Warn("Failed to create ProvisionedExternalId", "err", err) + } } // make sure db version is in sync with json model version diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index ec6c404e641..a3ddf76a7b8 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -11,6 +11,7 @@ import ( m "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/provisioning" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" @@ -43,8 +44,8 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) - bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { - query.Result = false + bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error { + query.Result = nil return nil }) @@ -198,8 +199,8 @@ func TestDashboardApiEndpoint(t *testing.T) { fakeDash.HasAcl = true setting.ViewersCanEdit = false - bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { - query.Result = false + bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error { + query.Result = nil return nil }) @@ -235,6 +236,10 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) + hs := &HTTPServer{ + Cfg: setting.NewCfg(), + } + // This tests six scenarios: // 1. user is an org viewer AND has no permissions for this dashboard // 2. user is an org editor AND has no permissions for this dashboard @@ -247,7 +252,7 @@ func TestDashboardApiEndpoint(t *testing.T) { role := m.ROLE_VIEWER loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - sc.handlerFunc = GetDashboard + sc.handlerFunc = hs.GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() Convey("Should lookup dashboard by slug", func() { @@ -260,7 +265,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - sc.handlerFunc = GetDashboard + sc.handlerFunc = hs.GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() Convey("Should lookup dashboard by uid", func() { @@ -305,7 +310,7 @@ func TestDashboardApiEndpoint(t *testing.T) { role := m.ROLE_EDITOR loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - sc.handlerFunc = GetDashboard + sc.handlerFunc = hs.GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() Convey("Should lookup dashboard by slug", func() { @@ -318,7 +323,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - sc.handlerFunc = GetDashboard + sc.handlerFunc = hs.GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() Convey("Should lookup dashboard by uid", func() { @@ -636,8 +641,8 @@ func TestDashboardApiEndpoint(t *testing.T) { dashTwo.FolderId = 3 dashTwo.HasAcl = false - bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { - query.Result = false + bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error { + query.Result = nil return nil }) @@ -766,8 +771,8 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) - bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { - query.Result = false + bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error { + query.Result = nil return nil }) @@ -905,12 +910,12 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) bus.AddHandler("test", func(query *m.GetDashboardQuery) error { - query.Result = &m.Dashboard{Id: 1} + query.Result = &m.Dashboard{Id: 1, Data: &simplejson.Json{}} return nil }) - bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { - query.Result = true + bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error { + query.Result = &m.DashboardProvisioning{ExternalId: "/tmp/grafana/dashboards/test/dashboard1.json"} return nil }) @@ -940,11 +945,32 @@ func TestDashboardApiEndpoint(t *testing.T) { So(result.Get("error").MustString(), ShouldEqual, m.ErrDashboardCannotDeleteProvisionedDashboard.Error()) }) }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", m.ROLE_EDITOR, func(sc *scenarioContext) { + mock := provisioning.NewProvisioningServiceMock() + mock.GetDashboardProvisionerResolvedPathFunc = func(name string) string { + return "/tmp/grafana/dashboards" + } + + dash := GetDashboardShouldReturn200WithConfig(sc, mock) + + Convey("Should return relative path to provisioning file", func() { + So(dash.Meta.ProvisionedExternalId, ShouldEqual, "test/dashboard1.json") + }) + }) }) } -func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta { - CallGetDashboard(sc) +func GetDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningService ProvisioningService) dtos.DashboardFullWithMeta { + if provisioningService == nil { + provisioningService = provisioning.NewProvisioningServiceMock() + } + + hs := &HTTPServer{ + Cfg: setting.NewCfg(), + ProvisioningService: provisioningService, + } + CallGetDashboard(sc, hs) So(sc.resp.Code, ShouldEqual, 200) @@ -955,8 +981,13 @@ func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta return dash } -func CallGetDashboard(sc *scenarioContext) { - sc.handlerFunc = GetDashboard +func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta { + return GetDashboardShouldReturn200WithConfig(sc, nil) +} + +func CallGetDashboard(sc *scenarioContext, hs *HTTPServer) { + + sc.handlerFunc = hs.GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go index 39a6dca580d..c30e0dbcad2 100644 --- a/pkg/api/dtos/dashboard.go +++ b/pkg/api/dtos/dashboard.go @@ -7,28 +7,29 @@ import ( ) type DashboardMeta struct { - IsStarred bool `json:"isStarred,omitempty"` - IsHome bool `json:"isHome,omitempty"` - IsSnapshot bool `json:"isSnapshot,omitempty"` - Type string `json:"type,omitempty"` - CanSave bool `json:"canSave"` - CanEdit bool `json:"canEdit"` - CanAdmin bool `json:"canAdmin"` - CanStar bool `json:"canStar"` - Slug string `json:"slug"` - Url string `json:"url"` - Expires time.Time `json:"expires"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - UpdatedBy string `json:"updatedBy"` - CreatedBy string `json:"createdBy"` - Version int `json:"version"` - HasAcl bool `json:"hasAcl"` - IsFolder bool `json:"isFolder"` - FolderId int64 `json:"folderId"` - FolderTitle string `json:"folderTitle"` - FolderUrl string `json:"folderUrl"` - Provisioned bool `json:"provisioned"` + IsStarred bool `json:"isStarred,omitempty"` + IsHome bool `json:"isHome,omitempty"` + IsSnapshot bool `json:"isSnapshot,omitempty"` + Type string `json:"type,omitempty"` + CanSave bool `json:"canSave"` + CanEdit bool `json:"canEdit"` + CanAdmin bool `json:"canAdmin"` + CanStar bool `json:"canStar"` + Slug string `json:"slug"` + Url string `json:"url"` + Expires time.Time `json:"expires"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + UpdatedBy string `json:"updatedBy"` + CreatedBy string `json:"createdBy"` + Version int `json:"version"` + HasAcl bool `json:"hasAcl"` + IsFolder bool `json:"isFolder"` + FolderId int64 `json:"folderId"` + FolderTitle string `json:"folderTitle"` + FolderUrl string `json:"folderUrl"` + Provisioned bool `json:"provisioned"` + ProvisionedExternalId string `json:"provisionedExternalId"` } type DashboardFullWithMeta struct { diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index a8e92014a8f..7c84d579e4a 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -25,13 +25,12 @@ import ( "github.com/grafana/grafana/pkg/services/cache" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/hooks" - "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" - macaron "gopkg.in/macaron.v1" + "gopkg.in/macaron.v1" ) func init() { @@ -42,6 +41,13 @@ func init() { }) } +type ProvisioningService interface { + ProvisionDatasources() error + ProvisionNotifications() error + ProvisionDashboards() error + GetDashboardProvisionerResolvedPath(name string) string +} + type HTTPServer struct { log log.Logger macaron *macaron.Macaron @@ -49,17 +55,17 @@ type HTTPServer struct { streamManager *live.StreamManager httpSrv *http.Server - RouteRegister routing.RouteRegister `inject:""` - Bus bus.Bus `inject:""` - RenderService rendering.Service `inject:""` - Cfg *setting.Cfg `inject:""` - HooksService *hooks.HooksService `inject:""` - CacheService *cache.CacheService `inject:""` - DatasourceCache datasources.CacheService `inject:""` - AuthTokenService models.UserTokenService `inject:""` - QuotaService *quota.QuotaService `inject:""` - RemoteCacheService *remotecache.RemoteCache `inject:""` - ProvisioningService provisioning.ProvisioningService `inject:""` + RouteRegister routing.RouteRegister `inject:""` + Bus bus.Bus `inject:""` + RenderService rendering.Service `inject:""` + Cfg *setting.Cfg `inject:""` + HooksService *hooks.HooksService `inject:""` + CacheService *cache.CacheService `inject:""` + DatasourceCache datasources.CacheService `inject:""` + AuthTokenService models.UserTokenService `inject:""` + QuotaService *quota.QuotaService `inject:""` + RemoteCacheService *remotecache.RemoteCache `inject:""` + ProvisioningService ProvisioningService `inject:""` } func (hs *HTTPServer) Init() error { diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index e54d0c11453..60677c9b6f6 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -323,15 +323,13 @@ type GetDashboardSlugByIdQuery struct { Result string } -type IsDashboardProvisionedQuery struct { +type GetProvisionedDashboardDataByIdQuery struct { DashboardId int64 - - Result bool + Result *DashboardProvisioning } type GetProvisionedDashboardDataQuery struct { - Name string - + Name string Result []*DashboardProvisioning } diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index cdf3d93a800..884d993f43c 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -24,6 +24,7 @@ type DashboardProvisioningService interface { SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) + GetProvisionedDashboardDataByDashboardId(dashboardId int64) (*models.DashboardProvisioning, error) UnprovisionDashboard(dashboardId int64) error DeleteProvisionedDashboard(dashboardId int64, orgId int64) error } @@ -37,7 +38,9 @@ var NewService = func() DashboardService { // NewProvisioningService factory for creating a new dashboard provisioning service var NewProvisioningService = func() DashboardProvisioningService { - return &dashboardServiceImpl{} + return &dashboardServiceImpl{ + log: log.New("dashboard-provisioning-service"), + } } type SaveDashboardDTO struct { @@ -65,6 +68,16 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*mod return cmd.Result, nil } +func (dr *dashboardServiceImpl) GetProvisionedDashboardDataByDashboardId(dashboardId int64) (*models.DashboardProvisioning, error) { + cmd := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: dashboardId} + err := bus.Dispatch(cmd) + if err != nil { + return nil, err + } + + return cmd.Result, nil +} + func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) { dash := dto.Dashboard @@ -123,14 +136,12 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, } if validateProvisionedDashboard { - isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dash.Id} - err := bus.Dispatch(isDashboardProvisioned) - + provisionedData, err := dr.GetProvisionedDashboardDataByDashboardId(dash.Id) if err != nil { return nil, err } - if isDashboardProvisioned.Result { + if provisionedData != nil { return nil, models.ErrDashboardCannotSaveProvisionedDashboard } } @@ -258,13 +269,12 @@ func (dr *dashboardServiceImpl) DeleteProvisionedDashboard(dashboardId int64, or func (dr *dashboardServiceImpl) deleteDashboard(dashboardId int64, orgId int64, validateProvisionedDashboard bool) error { if validateProvisionedDashboard { - isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dashboardId} - err := bus.Dispatch(isDashboardProvisioned) + provisionedData, err := dr.GetProvisionedDashboardDataByDashboardId(dashboardId) if err != nil { return errutil.Wrap("failed to check if dashboard is provisioned", err) } - if isDashboardProvisioned.Result { + if provisionedData != nil { return models.ErrDashboardCannotDeleteProvisionedDashboard } } diff --git a/pkg/services/dashboards/dashboard_service_test.go b/pkg/services/dashboards/dashboard_service_test.go index 968c3f9a3b4..db3ef997d65 100644 --- a/pkg/services/dashboards/dashboard_service_test.go +++ b/pkg/services/dashboards/dashboard_service_test.go @@ -55,8 +55,8 @@ func TestDashboardService(t *testing.T) { return nil }) - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { - cmd.Result = false + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { + cmd.Result = nil return nil }) @@ -85,9 +85,9 @@ func TestDashboardService(t *testing.T) { Convey("Should return validation error if dashboard is provisioned", func() { provisioningValidated := false - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { provisioningValidated = true - cmd.Result = true + cmd.Result = &models.DashboardProvisioning{} return nil }) @@ -109,8 +109,8 @@ func TestDashboardService(t *testing.T) { }) Convey("Should return validation error if alert data is invalid", func() { - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { - cmd.Result = false + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { + cmd.Result = nil return nil }) @@ -129,9 +129,9 @@ func TestDashboardService(t *testing.T) { Convey("Should not return validation error if dashboard is provisioned", func() { provisioningValidated := false - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { provisioningValidated = true - cmd.Result = true + cmd.Result = &models.DashboardProvisioning{} return nil }) @@ -166,9 +166,9 @@ func TestDashboardService(t *testing.T) { Convey("Should return validation error if dashboard is provisioned", func() { provisioningValidated := false - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { provisioningValidated = true - cmd.Result = true + cmd.Result = &models.DashboardProvisioning{} return nil }) @@ -241,8 +241,12 @@ type Result struct { } func setupDeleteHandlers(provisioned bool) *Result { - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { - cmd.Result = provisioned + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { + if provisioned { + cmd.Result = &models.DashboardProvisioning{} + } else { + cmd.Result = nil + } return nil }) diff --git a/pkg/services/dashboards/folder_service_test.go b/pkg/services/dashboards/folder_service_test.go index 4c9cecd3352..bdf7556413f 100644 --- a/pkg/services/dashboards/folder_service_test.go +++ b/pkg/services/dashboards/folder_service_test.go @@ -112,8 +112,9 @@ func TestFolderService(t *testing.T) { provisioningValidated := false - bus.AddHandler("test", func(query *models.IsDashboardProvisionedQuery) error { + bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { provisioningValidated = true + query.Result = nil return nil }) diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index b7e5539c3b0..fd3a824420c 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -7,18 +7,11 @@ import ( "github.com/pkg/errors" ) -type DashboardProvisioner interface { - Provision() error - PollChanges(ctx context.Context) -} - type DashboardProvisionerImpl struct { log log.Logger fileReaders []*fileReader } -type DashboardProvisionerFactory func(string) (DashboardProvisioner, error) - func NewDashboardProvisionerImpl(configDirectory string) (*DashboardProvisionerImpl, error) { logger := log.New("provisioning.dashboard") cfgReader := &configReader{path: configDirectory, log: logger} @@ -61,6 +54,17 @@ func (provider *DashboardProvisionerImpl) PollChanges(ctx context.Context) { } } +// GetProvisionerResolvedPath returns resolved path for the specified provisioner name. Can be used to generate +// relative path to provisioning file from it's external_id. +func (provider *DashboardProvisionerImpl) GetProvisionerResolvedPath(name string) string { + for _, reader := range provider.fileReaders { + if reader.Cfg.Name == name { + return reader.resolvedPath() + } + } + return "" +} + func getFileReaders(configs []*DashboardsAsConfig, logger log.Logger) ([]*fileReader, error) { var readers []*fileReader diff --git a/pkg/services/provisioning/dashboards/dashboard_mock.go b/pkg/services/provisioning/dashboards/dashboard_mock.go index 5cdaab9be70..303338106e6 100644 --- a/pkg/services/provisioning/dashboards/dashboard_mock.go +++ b/pkg/services/provisioning/dashboards/dashboard_mock.go @@ -3,14 +3,16 @@ package dashboards import "context" type Calls struct { - Provision []interface{} - PollChanges []interface{} + Provision []interface{} + PollChanges []interface{} + GetProvisionerResolvedPath []interface{} } type DashboardProvisionerMock struct { - Calls *Calls - ProvisionFunc func() error - PollChangesFunc func(ctx context.Context) + Calls *Calls + ProvisionFunc func() error + PollChangesFunc func(ctx context.Context) + GetProvisionerResolvedPathFunc func(name string) string } func NewDashboardProvisionerMock() *DashboardProvisionerMock { @@ -34,3 +36,12 @@ func (dpm *DashboardProvisionerMock) PollChanges(ctx context.Context) { dpm.PollChangesFunc(ctx) } } + +func (dpm *DashboardProvisionerMock) GetProvisionerResolvedPath(name string) string { + dpm.Calls.PollChanges = append(dpm.Calls.GetProvisionerResolvedPath, name) + if dpm.GetProvisionerResolvedPathFunc != nil { + return dpm.GetProvisionerResolvedPathFunc(name) + } else { + return "" + } +} diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 62709ce6ba9..96da9c8f6df 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -70,7 +70,7 @@ func (fr *fileReader) pollChanges(ctx context.Context) { // to the database. func (fr *fileReader) startWalkingDisk() error { fr.log.Debug("Start walking disk", "path", fr.Path) - resolvedPath := fr.resolvePath(fr.Path) + resolvedPath := fr.resolvedPath() if _, err := os.Stat(resolvedPath); err != nil { if os.IsNotExist(err) { return err @@ -329,24 +329,23 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, }, nil } -func (fr *fileReader) resolvePath(path string) string { - if _, err := os.Stat(path); os.IsNotExist(err) { +func (fr *fileReader) resolvedPath() string { + if _, err := os.Stat(fr.Path); os.IsNotExist(err) { fr.log.Error("Cannot read directory", "error", err) } - copy := path - path, err := filepath.Abs(path) + path, err := filepath.Abs(fr.Path) if err != nil { - fr.log.Error("Could not create absolute path", "path", copy, "error", err) + fr.log.Error("Could not create absolute path", "path", fr.Path, "error", err) } path, err = filepath.EvalSymlinks(path) if err != nil { - fr.log.Error("Failed to read content of symlinked path", "path", copy, "error", err) + fr.log.Error("Failed to read content of symlinked path", "path", fr.Path, "error", err) } if path == "" { - path = copy + path = fr.Path fr.log.Info("falling back to original path due to EvalSymlink/Abs failure") } return path diff --git a/pkg/services/provisioning/dashboards/file_reader_linux_test.go b/pkg/services/provisioning/dashboards/file_reader_linux_test.go index 77f488ebcfb..d62a59a4f4c 100644 --- a/pkg/services/provisioning/dashboards/file_reader_linux_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_linux_test.go @@ -33,7 +33,7 @@ func TestProvsionedSymlinkedFolder(t *testing.T) { t.Errorf("expected err to be nil") } - resolvedPath := reader.resolvePath(reader.Path) + resolvedPath := reader.resolvedPath() if resolvedPath != want { t.Errorf("got %s want %s", resolvedPath, want) } diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index 8c0a04a808a..efc6052cbe7 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -70,7 +70,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) So(err, ShouldBeNil) - resolvedPath := reader.resolvePath(reader.Path) + resolvedPath := reader.resolvedPath() So(filepath.IsAbs(resolvedPath), ShouldBeTrue) }) }) @@ -435,6 +435,10 @@ func (s *fakeDashboardProvisioningService) DeleteProvisionedDashboard(dashboardI return nil } +func (s *fakeDashboardProvisioningService) GetProvisionedDashboardDataByDashboardId(dashboardId int64) (*models.DashboardProvisioning, error) { + return nil, nil +} + func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error { for _, d := range fakeService.getDashboard { if d.Slug == cmd.Slug { diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 7cc4b993324..29f2d139164 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -15,9 +15,17 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +type DashboardProvisioner interface { + Provision() error + PollChanges(ctx context.Context) + GetProvisionerResolvedPath(name string) string +} + +type DashboardProvisionerFactory func(string) (DashboardProvisioner, error) + func init() { registry.RegisterService(NewProvisioningServiceImpl( - func(path string) (dashboards.DashboardProvisioner, error) { + func(path string) (DashboardProvisioner, error) { return dashboards.NewDashboardProvisionerImpl(path) }, notifiers.Provision, @@ -25,14 +33,8 @@ func init() { )) } -type ProvisioningService interface { - ProvisionDatasources() error - ProvisionNotifications() error - ProvisionDashboards() error -} - func NewProvisioningServiceImpl( - newDashboardProvisioner dashboards.DashboardProvisionerFactory, + newDashboardProvisioner DashboardProvisionerFactory, provisionNotifiers func(string) error, provisionDatasources func(string) error, ) *provisioningServiceImpl { @@ -48,8 +50,8 @@ type provisioningServiceImpl struct { Cfg *setting.Cfg `inject:""` log log.Logger pollingCtxCancel context.CancelFunc - newDashboardProvisioner dashboards.DashboardProvisionerFactory - dashboardProvisioner dashboards.DashboardProvisioner + newDashboardProvisioner DashboardProvisionerFactory + dashboardProvisioner DashboardProvisioner provisionNotifiers func(string) error provisionDatasources func(string) error mutex sync.Mutex @@ -131,6 +133,10 @@ func (ps *provisioningServiceImpl) ProvisionDashboards() error { return nil } +func (ps *provisioningServiceImpl) GetDashboardProvisionerResolvedPath(name string) string { + return ps.dashboardProvisioner.GetProvisionerResolvedPath(name) +} + func (ps *provisioningServiceImpl) cancelPolling() { if ps.pollingCtxCancel != nil { ps.log.Debug("Stop polling for dashboard changes") diff --git a/pkg/services/provisioning/provisioning_mock.go b/pkg/services/provisioning/provisioning_mock.go new file mode 100644 index 00000000000..7977e59b2e4 --- /dev/null +++ b/pkg/services/provisioning/provisioning_mock.go @@ -0,0 +1,58 @@ +package provisioning + +type Calls struct { + ProvisionDatasources []interface{} + ProvisionNotifications []interface{} + ProvisionDashboards []interface{} + GetDashboardProvisionerResolvedPath []interface{} +} + +type ProvisioningServiceMock struct { + Calls *Calls + ProvisionDatasourcesFunc func() error + ProvisionNotificationsFunc func() error + ProvisionDashboardsFunc func() error + GetDashboardProvisionerResolvedPathFunc func(name string) string +} + +func NewProvisioningServiceMock() *ProvisioningServiceMock { + return &ProvisioningServiceMock{ + Calls: &Calls{}, + } +} + +func (mock *ProvisioningServiceMock) ProvisionDatasources() error { + mock.Calls.ProvisionDatasources = append(mock.Calls.ProvisionDatasources, nil) + if mock.ProvisionDatasourcesFunc != nil { + return mock.ProvisionDatasourcesFunc() + } else { + return nil + } +} + +func (mock *ProvisioningServiceMock) ProvisionNotifications() error { + mock.Calls.ProvisionNotifications = append(mock.Calls.ProvisionNotifications, nil) + if mock.ProvisionNotificationsFunc != nil { + return mock.ProvisionNotificationsFunc() + } else { + return nil + } +} + +func (mock *ProvisioningServiceMock) ProvisionDashboards() error { + mock.Calls.ProvisionDashboards = append(mock.Calls.ProvisionDashboards, nil) + if mock.ProvisionDashboardsFunc != nil { + return mock.ProvisionDashboardsFunc() + } else { + return nil + } +} + +func (mock *ProvisioningServiceMock) GetDashboardProvisionerResolvedPath(name string) string { + mock.Calls.GetDashboardProvisionerResolvedPath = append(mock.Calls.GetDashboardProvisionerResolvedPath, name) + if mock.GetDashboardProvisionerResolvedPathFunc != nil { + return mock.GetDashboardProvisionerResolvedPathFunc(name) + } else { + return "" + } +} diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go index b959aa66087..119fa7261ca 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -92,7 +92,7 @@ func setup() *serviceTestStruct { } serviceTest.service = NewProvisioningServiceImpl( - func(path string) (dashboards.DashboardProvisioner, error) { + func(path string) (DashboardProvisioner, error) { return serviceTest.mock, nil }, nil, diff --git a/pkg/services/sqlstore/dashboard_provisioning.go b/pkg/services/sqlstore/dashboard_provisioning.go index b6c20682fae..48238477ce9 100644 --- a/pkg/services/sqlstore/dashboard_provisioning.go +++ b/pkg/services/sqlstore/dashboard_provisioning.go @@ -19,16 +19,16 @@ type DashboardExtras struct { Value string } -func GetProvisionedDataByDashboardId(cmd *models.IsDashboardProvisionedQuery) error { +func GetProvisionedDataByDashboardId(cmd *models.GetProvisionedDashboardDataByIdQuery) error { result := &models.DashboardProvisioning{} exist, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result) if err != nil { return err } - - cmd.Result = exist - + if exist { + cmd.Result = result + } return nil } diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go index 82ac294349c..b58829c92f8 100644 --- a/pkg/services/sqlstore/dashboard_provisioning_test.go +++ b/pkg/services/sqlstore/dashboard_provisioning_test.go @@ -65,20 +65,20 @@ func TestDashboardProvisioningTest(t *testing.T) { }) Convey("Can query for one provisioned dashboard", func() { - query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id} + query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: cmd.Result.Id} err := GetProvisionedDataByDashboardId(query) So(err, ShouldBeNil) - So(query.Result, ShouldBeTrue) + So(query.Result, ShouldNotBeNil) }) Convey("Can query for none provisioned dashboard", func() { - query := &models.IsDashboardProvisionedQuery{DashboardId: 3000} + query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: 3000} err := GetProvisionedDataByDashboardId(query) So(err, ShouldBeNil) - So(query.Result, ShouldBeFalse) + So(query.Result, ShouldBeNil) }) Convey("Deleting folder should delete provision meta data", func() { @@ -89,11 +89,11 @@ func TestDashboardProvisioningTest(t *testing.T) { So(DeleteDashboard(deleteCmd), ShouldBeNil) - query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id} + query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: cmd.Result.Id} err = GetProvisionedDataByDashboardId(query) So(err, ShouldBeNil) - So(query.Result, ShouldBeFalse) + So(query.Result, ShouldBeNil) }) Convey("UnprovisionDashboard should delete provisioning metadata", func() { @@ -103,11 +103,11 @@ func TestDashboardProvisioningTest(t *testing.T) { So(UnprovisionDashboard(unprovisionCmd), ShouldBeNil) - query := &models.IsDashboardProvisionedQuery{DashboardId: dashId} + query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: dashId} err = GetProvisionedDataByDashboardId(query) So(err, ShouldBeNil) - So(query.Result, ShouldBeFalse) + So(query.Result, ShouldBeNil) }) }) }) diff --git a/pkg/services/sqlstore/dashboard_service_integration_test.go b/pkg/services/sqlstore/dashboard_service_integration_test.go index a4e76aca340..82e0d212130 100644 --- a/pkg/services/sqlstore/dashboard_service_integration_test.go +++ b/pkg/services/sqlstore/dashboard_service_integration_test.go @@ -27,8 +27,8 @@ func TestIntegratedDashboardService(t *testing.T) { return nil }) - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { - cmd.Result = false + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { + cmd.Result = nil return nil }) diff --git a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts index c0332e61950..2fb9d76daa3 100755 --- a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts +++ b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts @@ -192,6 +192,8 @@ export class SettingsCtrl { text2: ` See documentation for more information about provisioning. +
+ File path: ${this.dashboard.meta.provisionedExternalId} `, text2htmlBind: true, icon: 'fa-trash', diff --git a/public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.ts b/public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.ts index 851644d3f14..ca85c962d1a 100644 --- a/public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.ts +++ b/public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.ts @@ -1,6 +1,7 @@ import angular from 'angular'; import { saveAs } from 'file-saver'; import coreModule from 'app/core/core_module'; +import { DashboardModel } from '../../state'; const template = `