Просмотр исходного кода

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
Andrej Ocenas 6 лет назад
Родитель
Сommit
eb82a75668

+ 2 - 2
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))

+ 13 - 4
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

+ 50 - 19
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()
 }
 

+ 23 - 22
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 {

+ 19 - 13
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 {

+ 3 - 5
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
 }
 

+ 18 - 8
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
 		}
 	}

+ 16 - 12
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
 	})
 

+ 2 - 1
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
 			})
 

+ 11 - 7
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
 

+ 16 - 5
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 ""
+	}
+}

+ 7 - 8
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

+ 1 - 1
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)
 	}

+ 5 - 1
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 {

+ 16 - 10
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")

+ 58 - 0
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 ""
+	}
+}

+ 1 - 1
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,

+ 4 - 4
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
 }
 

+ 8 - 8
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)
 			})
 		})
 	})

+ 2 - 2
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
 			})
 

+ 2 - 0
public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts

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

+ 7 - 1
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 = `
 <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">
       documentation</a> for more information about provisioning.</i>
     </small>
+    <div class="p-t-1">
+      File path: {{ctrl.dashboardModel.meta.provisionedExternalId}}
+    </div>
     <div class="p-t-2">
       <div class="gf-form">
         <code-editor content="ctrl.dashboardJson" data-mode="json" data-max-lines=15></code-editor>
@@ -41,12 +45,14 @@ const template = `
 
 export class SaveProvisionedDashboardModalCtrl {
   dash: any;
+  dashboardModel: DashboardModel;
   dashboardJson: string;
   dismiss: () => void;
 
   /** @ngInject */
   constructor(dashboardSrv) {
-    this.dash = dashboardSrv.getCurrent().getSaveModelClone();
+    this.dashboardModel = dashboardSrv.getCurrent();
+    this.dash = this.dashboardModel.getSaveModelClone();
     delete this.dash.id;
     this.dashboardJson = angular.toJson(this.dash, true);
   }

+ 1 - 0
public/app/types/dashboard.ts

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