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
This commit is contained in:
Andrej Ocenas 2019-04-30 13:32:18 +02:00 committed by GitHub
parent 76ab0aa059
commit eb82a75668
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 285 additions and 134 deletions

View File

@ -283,10 +283,10 @@ func (hs *HTTPServer) registerRoutes() {
// Dashboard // Dashboard
apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) { 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.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.Delete("/db/:slug", Wrap(DeleteDashboardBySlug))
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff)) dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff))

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"path/filepath"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards" "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) 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")) dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid"))
if rsp != nil { if rsp != nil {
return rsp return rsp
@ -106,14 +107,22 @@ func GetDashboard(c *m.ReqContext) Response {
meta.FolderUrl = query.Result.GetUrl() meta.FolderUrl = query.Result.GetUrl()
} }
isDashboardProvisioned := &m.IsDashboardProvisionedQuery{DashboardId: dash.Id} provisioningData, err := dashboards.NewProvisioningService().GetProvisionedDashboardDataByDashboardId(dash.Id)
err = bus.Dispatch(isDashboardProvisioned)
if err != nil { if err != nil {
return Error(500, "Error while checking if dashboard is provisioned", err) return Error(500, "Error while checking if dashboard is provisioned", err)
} }
if isDashboardProvisioned.Result { if provisioningData != nil {
meta.Provisioned = true 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 // make sure db version is in sync with json model version

View File

@ -11,6 +11,7 @@ import (
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
@ -43,8 +44,8 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil return nil
}) })
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error {
query.Result = false query.Result = nil
return nil return nil
}) })
@ -198,8 +199,8 @@ func TestDashboardApiEndpoint(t *testing.T) {
fakeDash.HasAcl = true fakeDash.HasAcl = true
setting.ViewersCanEdit = false setting.ViewersCanEdit = false
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error {
query.Result = false query.Result = nil
return nil return nil
}) })
@ -235,6 +236,10 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil return nil
}) })
hs := &HTTPServer{
Cfg: setting.NewCfg(),
}
// This tests six scenarios: // This tests six scenarios:
// 1. user is an org viewer AND has no permissions for this dashboard // 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 // 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 role := m.ROLE_VIEWER
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { 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() sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should lookup dashboard by slug", func() { 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) { 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() sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should lookup dashboard by uid", func() { Convey("Should lookup dashboard by uid", func() {
@ -305,7 +310,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
role := m.ROLE_EDITOR role := m.ROLE_EDITOR
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { 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() sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should lookup dashboard by slug", func() { 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) { 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() sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should lookup dashboard by uid", func() { Convey("Should lookup dashboard by uid", func() {
@ -636,8 +641,8 @@ func TestDashboardApiEndpoint(t *testing.T) {
dashTwo.FolderId = 3 dashTwo.FolderId = 3
dashTwo.HasAcl = false dashTwo.HasAcl = false
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error {
query.Result = false query.Result = nil
return nil return nil
}) })
@ -766,8 +771,8 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil return nil
}) })
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error {
query.Result = false query.Result = nil
return nil return nil
}) })
@ -905,12 +910,12 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil return nil
}) })
bus.AddHandler("test", func(query *m.GetDashboardQuery) error { 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 return nil
}) })
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error {
query.Result = true query.Result = &m.DashboardProvisioning{ExternalId: "/tmp/grafana/dashboards/test/dashboard1.json"}
return nil return nil
}) })
@ -940,11 +945,32 @@ func TestDashboardApiEndpoint(t *testing.T) {
So(result.Get("error").MustString(), ShouldEqual, m.ErrDashboardCannotDeleteProvisionedDashboard.Error()) 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 { func GetDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningService ProvisioningService) dtos.DashboardFullWithMeta {
CallGetDashboard(sc) if provisioningService == nil {
provisioningService = provisioning.NewProvisioningServiceMock()
}
hs := &HTTPServer{
Cfg: setting.NewCfg(),
ProvisioningService: provisioningService,
}
CallGetDashboard(sc, hs)
So(sc.resp.Code, ShouldEqual, 200) So(sc.resp.Code, ShouldEqual, 200)
@ -955,8 +981,13 @@ func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta
return dash return dash
} }
func CallGetDashboard(sc *scenarioContext) { func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
sc.handlerFunc = GetDashboard return GetDashboardShouldReturn200WithConfig(sc, nil)
}
func CallGetDashboard(sc *scenarioContext, hs *HTTPServer) {
sc.handlerFunc = hs.GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
} }

View File

@ -7,28 +7,29 @@ import (
) )
type DashboardMeta struct { type DashboardMeta struct {
IsStarred bool `json:"isStarred,omitempty"` IsStarred bool `json:"isStarred,omitempty"`
IsHome bool `json:"isHome,omitempty"` IsHome bool `json:"isHome,omitempty"`
IsSnapshot bool `json:"isSnapshot,omitempty"` IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"` CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"` CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"` CanAdmin bool `json:"canAdmin"`
CanStar bool `json:"canStar"` CanStar bool `json:"canStar"`
Slug string `json:"slug"` Slug string `json:"slug"`
Url string `json:"url"` Url string `json:"url"`
Expires time.Time `json:"expires"` Expires time.Time `json:"expires"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
Updated time.Time `json:"updated"` Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"` UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"` CreatedBy string `json:"createdBy"`
Version int `json:"version"` Version int `json:"version"`
HasAcl bool `json:"hasAcl"` HasAcl bool `json:"hasAcl"`
IsFolder bool `json:"isFolder"` IsFolder bool `json:"isFolder"`
FolderId int64 `json:"folderId"` FolderId int64 `json:"folderId"`
FolderTitle string `json:"folderTitle"` FolderTitle string `json:"folderTitle"`
FolderUrl string `json:"folderUrl"` FolderUrl string `json:"folderUrl"`
Provisioned bool `json:"provisioned"` Provisioned bool `json:"provisioned"`
ProvisionedExternalId string `json:"provisionedExternalId"`
} }
type DashboardFullWithMeta struct { type DashboardFullWithMeta struct {

View File

@ -25,13 +25,12 @@ import (
"github.com/grafana/grafana/pkg/services/cache" "github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks" "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/quota"
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
macaron "gopkg.in/macaron.v1" "gopkg.in/macaron.v1"
) )
func init() { func init() {
@ -42,6 +41,13 @@ func init() {
}) })
} }
type ProvisioningService interface {
ProvisionDatasources() error
ProvisionNotifications() error
ProvisionDashboards() error
GetDashboardProvisionerResolvedPath(name string) string
}
type HTTPServer struct { type HTTPServer struct {
log log.Logger log log.Logger
macaron *macaron.Macaron macaron *macaron.Macaron
@ -49,17 +55,17 @@ type HTTPServer struct {
streamManager *live.StreamManager streamManager *live.StreamManager
httpSrv *http.Server httpSrv *http.Server
RouteRegister routing.RouteRegister `inject:""` RouteRegister routing.RouteRegister `inject:""`
Bus bus.Bus `inject:""` Bus bus.Bus `inject:""`
RenderService rendering.Service `inject:""` RenderService rendering.Service `inject:""`
Cfg *setting.Cfg `inject:""` Cfg *setting.Cfg `inject:""`
HooksService *hooks.HooksService `inject:""` HooksService *hooks.HooksService `inject:""`
CacheService *cache.CacheService `inject:""` CacheService *cache.CacheService `inject:""`
DatasourceCache datasources.CacheService `inject:""` DatasourceCache datasources.CacheService `inject:""`
AuthTokenService models.UserTokenService `inject:""` AuthTokenService models.UserTokenService `inject:""`
QuotaService *quota.QuotaService `inject:""` QuotaService *quota.QuotaService `inject:""`
RemoteCacheService *remotecache.RemoteCache `inject:""` RemoteCacheService *remotecache.RemoteCache `inject:""`
ProvisioningService provisioning.ProvisioningService `inject:""` ProvisioningService ProvisioningService `inject:""`
} }
func (hs *HTTPServer) Init() error { func (hs *HTTPServer) Init() error {

View File

@ -323,15 +323,13 @@ type GetDashboardSlugByIdQuery struct {
Result string Result string
} }
type IsDashboardProvisionedQuery struct { type GetProvisionedDashboardDataByIdQuery struct {
DashboardId int64 DashboardId int64
Result *DashboardProvisioning
Result bool
} }
type GetProvisionedDashboardDataQuery struct { type GetProvisionedDashboardDataQuery struct {
Name string Name string
Result []*DashboardProvisioning Result []*DashboardProvisioning
} }

View File

@ -24,6 +24,7 @@ type DashboardProvisioningService interface {
SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error) SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error)
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
GetProvisionedDashboardDataByDashboardId(dashboardId int64) (*models.DashboardProvisioning, error)
UnprovisionDashboard(dashboardId int64) error UnprovisionDashboard(dashboardId int64) error
DeleteProvisionedDashboard(dashboardId int64, orgId 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 // NewProvisioningService factory for creating a new dashboard provisioning service
var NewProvisioningService = func() DashboardProvisioningService { var NewProvisioningService = func() DashboardProvisioningService {
return &dashboardServiceImpl{} return &dashboardServiceImpl{
log: log.New("dashboard-provisioning-service"),
}
} }
type SaveDashboardDTO struct { type SaveDashboardDTO struct {
@ -65,6 +68,16 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*mod
return cmd.Result, nil 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) { func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) {
dash := dto.Dashboard dash := dto.Dashboard
@ -123,14 +136,12 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
} }
if validateProvisionedDashboard { if validateProvisionedDashboard {
isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dash.Id} provisionedData, err := dr.GetProvisionedDashboardDataByDashboardId(dash.Id)
err := bus.Dispatch(isDashboardProvisioned)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if isDashboardProvisioned.Result { if provisionedData != nil {
return nil, models.ErrDashboardCannotSaveProvisionedDashboard 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 { func (dr *dashboardServiceImpl) deleteDashboard(dashboardId int64, orgId int64, validateProvisionedDashboard bool) error {
if validateProvisionedDashboard { if validateProvisionedDashboard {
isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dashboardId} provisionedData, err := dr.GetProvisionedDashboardDataByDashboardId(dashboardId)
err := bus.Dispatch(isDashboardProvisioned)
if err != nil { if err != nil {
return errutil.Wrap("failed to check if dashboard is provisioned", err) return errutil.Wrap("failed to check if dashboard is provisioned", err)
} }
if isDashboardProvisioned.Result { if provisionedData != nil {
return models.ErrDashboardCannotDeleteProvisionedDashboard return models.ErrDashboardCannotDeleteProvisionedDashboard
} }
} }

View File

@ -55,8 +55,8 @@ func TestDashboardService(t *testing.T) {
return nil return nil
}) })
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
cmd.Result = false cmd.Result = nil
return nil return nil
}) })
@ -85,9 +85,9 @@ func TestDashboardService(t *testing.T) {
Convey("Should return validation error if dashboard is provisioned", func() { Convey("Should return validation error if dashboard is provisioned", func() {
provisioningValidated := false provisioningValidated := false
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
provisioningValidated = true provisioningValidated = true
cmd.Result = true cmd.Result = &models.DashboardProvisioning{}
return nil return nil
}) })
@ -109,8 +109,8 @@ func TestDashboardService(t *testing.T) {
}) })
Convey("Should return validation error if alert data is invalid", func() { Convey("Should return validation error if alert data is invalid", func() {
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
cmd.Result = false cmd.Result = nil
return nil return nil
}) })
@ -129,9 +129,9 @@ func TestDashboardService(t *testing.T) {
Convey("Should not return validation error if dashboard is provisioned", func() { Convey("Should not return validation error if dashboard is provisioned", func() {
provisioningValidated := false provisioningValidated := false
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
provisioningValidated = true provisioningValidated = true
cmd.Result = true cmd.Result = &models.DashboardProvisioning{}
return nil return nil
}) })
@ -166,9 +166,9 @@ func TestDashboardService(t *testing.T) {
Convey("Should return validation error if dashboard is provisioned", func() { Convey("Should return validation error if dashboard is provisioned", func() {
provisioningValidated := false provisioningValidated := false
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
provisioningValidated = true provisioningValidated = true
cmd.Result = true cmd.Result = &models.DashboardProvisioning{}
return nil return nil
}) })
@ -241,8 +241,12 @@ type Result struct {
} }
func setupDeleteHandlers(provisioned bool) *Result { func setupDeleteHandlers(provisioned bool) *Result {
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
cmd.Result = provisioned if provisioned {
cmd.Result = &models.DashboardProvisioning{}
} else {
cmd.Result = nil
}
return nil return nil
}) })

View File

@ -112,8 +112,9 @@ func TestFolderService(t *testing.T) {
provisioningValidated := false provisioningValidated := false
bus.AddHandler("test", func(query *models.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error {
provisioningValidated = true provisioningValidated = true
query.Result = nil
return nil return nil
}) })

View File

@ -7,18 +7,11 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type DashboardProvisioner interface {
Provision() error
PollChanges(ctx context.Context)
}
type DashboardProvisionerImpl struct { type DashboardProvisionerImpl struct {
log log.Logger log log.Logger
fileReaders []*fileReader fileReaders []*fileReader
} }
type DashboardProvisionerFactory func(string) (DashboardProvisioner, error)
func NewDashboardProvisionerImpl(configDirectory string) (*DashboardProvisionerImpl, error) { func NewDashboardProvisionerImpl(configDirectory string) (*DashboardProvisionerImpl, error) {
logger := log.New("provisioning.dashboard") logger := log.New("provisioning.dashboard")
cfgReader := &configReader{path: configDirectory, log: logger} 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) { func getFileReaders(configs []*DashboardsAsConfig, logger log.Logger) ([]*fileReader, error) {
var readers []*fileReader var readers []*fileReader

View File

@ -3,14 +3,16 @@ package dashboards
import "context" import "context"
type Calls struct { type Calls struct {
Provision []interface{} Provision []interface{}
PollChanges []interface{} PollChanges []interface{}
GetProvisionerResolvedPath []interface{}
} }
type DashboardProvisionerMock struct { type DashboardProvisionerMock struct {
Calls *Calls Calls *Calls
ProvisionFunc func() error ProvisionFunc func() error
PollChangesFunc func(ctx context.Context) PollChangesFunc func(ctx context.Context)
GetProvisionerResolvedPathFunc func(name string) string
} }
func NewDashboardProvisionerMock() *DashboardProvisionerMock { func NewDashboardProvisionerMock() *DashboardProvisionerMock {
@ -34,3 +36,12 @@ func (dpm *DashboardProvisionerMock) PollChanges(ctx context.Context) {
dpm.PollChangesFunc(ctx) 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 ""
}
}

View File

@ -70,7 +70,7 @@ func (fr *fileReader) pollChanges(ctx context.Context) {
// to the database. // to the database.
func (fr *fileReader) startWalkingDisk() error { func (fr *fileReader) startWalkingDisk() error {
fr.log.Debug("Start walking disk", "path", fr.Path) 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 _, err := os.Stat(resolvedPath); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return err return err
@ -329,24 +329,23 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time,
}, nil }, nil
} }
func (fr *fileReader) resolvePath(path string) string { func (fr *fileReader) resolvedPath() string {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(fr.Path); os.IsNotExist(err) {
fr.log.Error("Cannot read directory", "error", err) fr.log.Error("Cannot read directory", "error", err)
} }
copy := path path, err := filepath.Abs(fr.Path)
path, err := filepath.Abs(path)
if err != nil { 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) path, err = filepath.EvalSymlinks(path)
if err != nil { 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 == "" { if path == "" {
path = copy path = fr.Path
fr.log.Info("falling back to original path due to EvalSymlink/Abs failure") fr.log.Info("falling back to original path due to EvalSymlink/Abs failure")
} }
return path return path

View File

@ -33,7 +33,7 @@ func TestProvsionedSymlinkedFolder(t *testing.T) {
t.Errorf("expected err to be nil") t.Errorf("expected err to be nil")
} }
resolvedPath := reader.resolvePath(reader.Path) resolvedPath := reader.resolvedPath()
if resolvedPath != want { if resolvedPath != want {
t.Errorf("got %s want %s", resolvedPath, want) t.Errorf("got %s want %s", resolvedPath, want)
} }

View File

@ -70,7 +70,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
So(err, ShouldBeNil) So(err, ShouldBeNil)
resolvedPath := reader.resolvePath(reader.Path) resolvedPath := reader.resolvedPath()
So(filepath.IsAbs(resolvedPath), ShouldBeTrue) So(filepath.IsAbs(resolvedPath), ShouldBeTrue)
}) })
}) })
@ -435,6 +435,10 @@ func (s *fakeDashboardProvisioningService) DeleteProvisionedDashboard(dashboardI
return nil return nil
} }
func (s *fakeDashboardProvisioningService) GetProvisionedDashboardDataByDashboardId(dashboardId int64) (*models.DashboardProvisioning, error) {
return nil, nil
}
func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error { func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
for _, d := range fakeService.getDashboard { for _, d := range fakeService.getDashboard {
if d.Slug == cmd.Slug { if d.Slug == cmd.Slug {

View File

@ -15,9 +15,17 @@ import (
"github.com/grafana/grafana/pkg/setting" "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() { func init() {
registry.RegisterService(NewProvisioningServiceImpl( registry.RegisterService(NewProvisioningServiceImpl(
func(path string) (dashboards.DashboardProvisioner, error) { func(path string) (DashboardProvisioner, error) {
return dashboards.NewDashboardProvisionerImpl(path) return dashboards.NewDashboardProvisionerImpl(path)
}, },
notifiers.Provision, notifiers.Provision,
@ -25,14 +33,8 @@ func init() {
)) ))
} }
type ProvisioningService interface {
ProvisionDatasources() error
ProvisionNotifications() error
ProvisionDashboards() error
}
func NewProvisioningServiceImpl( func NewProvisioningServiceImpl(
newDashboardProvisioner dashboards.DashboardProvisionerFactory, newDashboardProvisioner DashboardProvisionerFactory,
provisionNotifiers func(string) error, provisionNotifiers func(string) error,
provisionDatasources func(string) error, provisionDatasources func(string) error,
) *provisioningServiceImpl { ) *provisioningServiceImpl {
@ -48,8 +50,8 @@ type provisioningServiceImpl struct {
Cfg *setting.Cfg `inject:""` Cfg *setting.Cfg `inject:""`
log log.Logger log log.Logger
pollingCtxCancel context.CancelFunc pollingCtxCancel context.CancelFunc
newDashboardProvisioner dashboards.DashboardProvisionerFactory newDashboardProvisioner DashboardProvisionerFactory
dashboardProvisioner dashboards.DashboardProvisioner dashboardProvisioner DashboardProvisioner
provisionNotifiers func(string) error provisionNotifiers func(string) error
provisionDatasources func(string) error provisionDatasources func(string) error
mutex sync.Mutex mutex sync.Mutex
@ -131,6 +133,10 @@ func (ps *provisioningServiceImpl) ProvisionDashboards() error {
return nil return nil
} }
func (ps *provisioningServiceImpl) GetDashboardProvisionerResolvedPath(name string) string {
return ps.dashboardProvisioner.GetProvisionerResolvedPath(name)
}
func (ps *provisioningServiceImpl) cancelPolling() { func (ps *provisioningServiceImpl) cancelPolling() {
if ps.pollingCtxCancel != nil { if ps.pollingCtxCancel != nil {
ps.log.Debug("Stop polling for dashboard changes") ps.log.Debug("Stop polling for dashboard changes")

View File

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

View File

@ -92,7 +92,7 @@ func setup() *serviceTestStruct {
} }
serviceTest.service = NewProvisioningServiceImpl( serviceTest.service = NewProvisioningServiceImpl(
func(path string) (dashboards.DashboardProvisioner, error) { func(path string) (DashboardProvisioner, error) {
return serviceTest.mock, nil return serviceTest.mock, nil
}, },
nil, nil,

View File

@ -19,16 +19,16 @@ type DashboardExtras struct {
Value string Value string
} }
func GetProvisionedDataByDashboardId(cmd *models.IsDashboardProvisionedQuery) error { func GetProvisionedDataByDashboardId(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
result := &models.DashboardProvisioning{} result := &models.DashboardProvisioning{}
exist, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result) exist, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result)
if err != nil { if err != nil {
return err return err
} }
if exist {
cmd.Result = exist cmd.Result = result
}
return nil return nil
} }

View File

@ -65,20 +65,20 @@ func TestDashboardProvisioningTest(t *testing.T) {
}) })
Convey("Can query for one provisioned dashboard", func() { 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) err := GetProvisionedDataByDashboardId(query)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(query.Result, ShouldBeTrue) So(query.Result, ShouldNotBeNil)
}) })
Convey("Can query for none provisioned dashboard", func() { Convey("Can query for none provisioned dashboard", func() {
query := &models.IsDashboardProvisionedQuery{DashboardId: 3000} query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: 3000}
err := GetProvisionedDataByDashboardId(query) err := GetProvisionedDataByDashboardId(query)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(query.Result, ShouldBeFalse) So(query.Result, ShouldBeNil)
}) })
Convey("Deleting folder should delete provision meta data", func() { Convey("Deleting folder should delete provision meta data", func() {
@ -89,11 +89,11 @@ func TestDashboardProvisioningTest(t *testing.T) {
So(DeleteDashboard(deleteCmd), ShouldBeNil) So(DeleteDashboard(deleteCmd), ShouldBeNil)
query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id} query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: cmd.Result.Id}
err = GetProvisionedDataByDashboardId(query) err = GetProvisionedDataByDashboardId(query)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(query.Result, ShouldBeFalse) So(query.Result, ShouldBeNil)
}) })
Convey("UnprovisionDashboard should delete provisioning metadata", func() { Convey("UnprovisionDashboard should delete provisioning metadata", func() {
@ -103,11 +103,11 @@ func TestDashboardProvisioningTest(t *testing.T) {
So(UnprovisionDashboard(unprovisionCmd), ShouldBeNil) So(UnprovisionDashboard(unprovisionCmd), ShouldBeNil)
query := &models.IsDashboardProvisionedQuery{DashboardId: dashId} query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: dashId}
err = GetProvisionedDataByDashboardId(query) err = GetProvisionedDataByDashboardId(query)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(query.Result, ShouldBeFalse) So(query.Result, ShouldBeNil)
}) })
}) })
}) })

View File

@ -27,8 +27,8 @@ func TestIntegratedDashboardService(t *testing.T) {
return nil return nil
}) })
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
cmd.Result = false cmd.Result = nil
return nil return nil
}) })

View File

@ -192,6 +192,8 @@ export class SettingsCtrl {
text2: ` text2: `
<i>See <a class="external-link" href="http://docs.grafana.org/administration/provisioning/#dashboards" target="_blank"> <i>See <a class="external-link" href="http://docs.grafana.org/administration/provisioning/#dashboards" target="_blank">
documentation</a> for more information about provisioning.</i> documentation</a> for more information about provisioning.</i>
</br>
File path: ${this.dashboard.meta.provisionedExternalId}
`, `,
text2htmlBind: true, text2htmlBind: true,
icon: 'fa-trash', icon: 'fa-trash',

View File

@ -1,6 +1,7 @@
import angular from 'angular'; import angular from 'angular';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import { DashboardModel } from '../../state';
const template = ` const template = `
<div class="modal-body"> <div class="modal-body">
@ -21,6 +22,9 @@ const template = `
<i>See <a class="external-link" href="http://docs.grafana.org/administration/provisioning/#dashboards" target="_blank"> <i>See <a class="external-link" href="http://docs.grafana.org/administration/provisioning/#dashboards" target="_blank">
documentation</a> for more information about provisioning.</i> documentation</a> for more information about provisioning.</i>
</small> </small>
<div class="p-t-1">
File path: {{ctrl.dashboardModel.meta.provisionedExternalId}}
</div>
<div class="p-t-2"> <div class="p-t-2">
<div class="gf-form"> <div class="gf-form">
<code-editor content="ctrl.dashboardJson" data-mode="json" data-max-lines=15></code-editor> <code-editor content="ctrl.dashboardJson" data-mode="json" data-max-lines=15></code-editor>
@ -41,12 +45,14 @@ const template = `
export class SaveProvisionedDashboardModalCtrl { export class SaveProvisionedDashboardModalCtrl {
dash: any; dash: any;
dashboardModel: DashboardModel;
dashboardJson: string; dashboardJson: string;
dismiss: () => void; dismiss: () => void;
/** @ngInject */ /** @ngInject */
constructor(dashboardSrv) { constructor(dashboardSrv) {
this.dash = dashboardSrv.getCurrent().getSaveModelClone(); this.dashboardModel = dashboardSrv.getCurrent();
this.dash = this.dashboardModel.getSaveModelClone();
delete this.dash.id; delete this.dash.id;
this.dashboardJson = angular.toJson(this.dash, true); this.dashboardJson = angular.toJson(this.dash, true);
} }

View File

@ -26,6 +26,7 @@ export interface DashboardMeta {
canMakeEditable?: boolean; canMakeEditable?: boolean;
submenuEnabled?: boolean; submenuEnabled?: boolean;
provisioned?: boolean; provisioned?: boolean;
provisionedExternalId?: string;
focusPanelId?: number; focusPanelId?: number;
isStarred?: boolean; isStarred?: boolean;
showSettings?: boolean; showSettings?: boolean;