mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Refactor GetPluginDashboards/LoadPluginDashboard (#46316)
Refactors GetPluginDashboards/LoadPluginDashboard by moving database interaction from plugin management to the plugindashboards service. Fixes #44553 Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
This commit is contained in:
parent
d076cabb60
commit
6c7d326499
@ -40,6 +40,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -446,3 +447,31 @@ func mockRequestBody(v interface{}) io.ReadCloser {
|
||||
b, _ := json.Marshal(v)
|
||||
return io.NopCloser(bytes.NewReader(b))
|
||||
}
|
||||
|
||||
// APITestServerOption option func for customizing HTTPServer configuration
|
||||
// when setting up an API test server via SetupAPITestServer.
|
||||
type APITestServerOption func(hs *HTTPServer)
|
||||
|
||||
// SetupAPITestServer sets up a webtest.Server ready for testing all
|
||||
// routes registered via HTTPServer.registerRoutes().
|
||||
// Optionally customize HTTPServer configuration by providing APITestServerOption
|
||||
// option(s).
|
||||
func SetupAPITestServer(t *testing.T, opts ...APITestServerOption) *webtest.Server {
|
||||
t.Helper()
|
||||
|
||||
hs := &HTTPServer{
|
||||
RouteRegister: routing.NewRouteRegister(),
|
||||
Cfg: setting.NewCfg(),
|
||||
AccessControl: accesscontrolmock.New().WithDisabled(),
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
searchUsersService: &searchusers.OSSService{},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(hs)
|
||||
}
|
||||
|
||||
hs.registerRoutes()
|
||||
s := webtest.NewServer(t, hs.RouteRegister)
|
||||
return s
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert"
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/query"
|
||||
@ -100,7 +101,7 @@ type HTTPServer struct {
|
||||
PluginRequestValidator models.PluginRequestValidator
|
||||
pluginClient plugins.Client
|
||||
pluginStore plugins.Store
|
||||
pluginDashboardManager plugins.PluginDashboardManager
|
||||
pluginDashboardService plugindashboards.Service
|
||||
pluginStaticRouteResolver plugins.StaticRouteResolver
|
||||
pluginErrorResolver plugins.ErrorResolver
|
||||
SearchService search.Service
|
||||
@ -152,7 +153,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
renderService rendering.Service, licensing models.Licensing, hooksService *hooks.HooksService,
|
||||
cacheService *localcache.CacheService, sqlStore *sqlstore.SQLStore, alertEngine *alerting.AlertEngine,
|
||||
pluginRequestValidator models.PluginRequestValidator, pluginStaticRouteResolver plugins.StaticRouteResolver,
|
||||
pluginDashboardManager plugins.PluginDashboardManager, pluginStore plugins.Store, pluginClient plugins.Client,
|
||||
pluginDashboardService plugindashboards.Service, pluginStore plugins.Store, pluginClient plugins.Client,
|
||||
pluginErrorResolver plugins.ErrorResolver, settingsProvider setting.Provider,
|
||||
dataSourceCache datasources.CacheService, userTokenService models.UserTokenService,
|
||||
cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service, queryHistoryService queryhistory.Service,
|
||||
@ -191,7 +192,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
pluginClient: pluginClient,
|
||||
pluginStore: pluginStore,
|
||||
pluginStaticRouteResolver: pluginStaticRouteResolver,
|
||||
pluginDashboardManager: pluginDashboardManager,
|
||||
pluginDashboardService: pluginDashboardService,
|
||||
pluginErrorResolver: pluginErrorResolver,
|
||||
grafanaUpdateChecker: grafanaUpdateChecker,
|
||||
pluginsUpdateChecker: pluginsUpdateChecker,
|
||||
|
35
pkg/api/plugin_dashboards.go
Normal file
35
pkg/api/plugin_dashboards.go
Normal file
@ -0,0 +1,35 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
// GetPluginDashboards get plugin dashboards.
|
||||
//
|
||||
// /api/plugins/:pluginId/dashboards
|
||||
func (hs *HTTPServer) GetPluginDashboards(c *models.ReqContext) response.Response {
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
|
||||
listReq := &plugindashboards.ListPluginDashboardsRequest{
|
||||
OrgID: c.OrgId,
|
||||
PluginID: pluginID,
|
||||
}
|
||||
list, err := hs.pluginDashboardService.ListPluginDashboards(c.Req.Context(), listReq)
|
||||
if err != nil {
|
||||
var notFound plugins.NotFoundError
|
||||
if errors.As(err, ¬Found) {
|
||||
return response.Error(http.StatusNotFound, notFound.Error(), nil)
|
||||
}
|
||||
|
||||
return response.Error(http.StatusInternalServerError, "Failed to get plugin dashboards", err)
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, list.Items)
|
||||
}
|
128
pkg/api/plugin_dashboards_test.go
Normal file
128
pkg/api/plugin_dashboards_test.go
Normal file
@ -0,0 +1,128 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetPluginDashboards(t *testing.T) {
|
||||
const existingPluginID = "existing-plugin"
|
||||
pluginDashboardService := &pluginDashboardServiceMock{
|
||||
pluginDashboards: map[string][]*plugindashboards.PluginDashboard{
|
||||
existingPluginID: {
|
||||
{
|
||||
PluginId: existingPluginID,
|
||||
UID: "a",
|
||||
Title: "A",
|
||||
},
|
||||
{
|
||||
PluginId: existingPluginID,
|
||||
UID: "b",
|
||||
Title: "B",
|
||||
},
|
||||
},
|
||||
},
|
||||
unexpectedErrors: map[string]error{
|
||||
"boom": fmt.Errorf("BOOM"),
|
||||
},
|
||||
}
|
||||
|
||||
s := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.pluginDashboardService = pluginDashboardService
|
||||
})
|
||||
|
||||
t.Run("Not signed in should return 404 Not Found", func(t *testing.T) {
|
||||
req := s.NewGetRequest("/api/plugins/test/dashboards")
|
||||
resp, err := s.Send(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Signed in and not org admin should return 403 Forbidden", func(t *testing.T) {
|
||||
user := &models.SignedInUser{
|
||||
UserId: 1,
|
||||
OrgRole: models.ROLE_EDITOR,
|
||||
}
|
||||
|
||||
resp, err := sendGetPluginDashboardsRequestForSignedInUser(t, s, existingPluginID, user)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Signed in and org admin", func(t *testing.T) {
|
||||
user := &models.SignedInUser{
|
||||
UserId: 1,
|
||||
OrgId: 1,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
}
|
||||
|
||||
t.Run("When plugin doesn't exist should return 404 Not Found", func(t *testing.T) {
|
||||
resp, err := sendGetPluginDashboardsRequestForSignedInUser(t, s, "not-exists", user)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("When result is unexpected error should return 500 Internal Server Error", func(t *testing.T) {
|
||||
resp, err := sendGetPluginDashboardsRequestForSignedInUser(t, s, "boom", user)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("When plugin exists should return 200 OK with expected payload", func(t *testing.T) {
|
||||
resp, err := sendGetPluginDashboardsRequestForSignedInUser(t, s, existingPluginID, user)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
bytes, err := ioutil.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
var listResp []*plugindashboards.PluginDashboard
|
||||
err = json.Unmarshal(bytes, &listResp)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, listResp)
|
||||
require.Len(t, listResp, 2)
|
||||
require.Equal(t, pluginDashboardService.pluginDashboards[existingPluginID], listResp)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func sendGetPluginDashboardsRequestForSignedInUser(t *testing.T, s *webtest.Server, pluginID string, user *models.SignedInUser) (*http.Response, error) {
|
||||
t.Helper()
|
||||
|
||||
req := s.NewGetRequest(fmt.Sprintf("/api/plugins/%s/dashboards", pluginID))
|
||||
webtest.RequestWithSignedInUser(req, user)
|
||||
return s.Send(req)
|
||||
}
|
||||
|
||||
type pluginDashboardServiceMock struct {
|
||||
plugindashboards.Service
|
||||
pluginDashboards map[string][]*plugindashboards.PluginDashboard
|
||||
unexpectedErrors map[string]error
|
||||
}
|
||||
|
||||
func (m *pluginDashboardServiceMock) ListPluginDashboards(ctx context.Context, req *plugindashboards.ListPluginDashboardsRequest) (*plugindashboards.ListPluginDashboardsResponse, error) {
|
||||
if pluginDashboards, exists := m.pluginDashboards[req.PluginID]; exists {
|
||||
return &plugindashboards.ListPluginDashboardsResponse{
|
||||
Items: pluginDashboards,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err, exists := m.unexpectedErrors[req.PluginID]; exists {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, plugins.NotFoundError{PluginID: req.PluginID}
|
||||
}
|
@ -182,22 +182,6 @@ func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext) response.Respons
|
||||
return response.Success("Plugin settings updated")
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) GetPluginDashboards(c *models.ReqContext) response.Response {
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
|
||||
list, err := hs.pluginDashboardManager.GetPluginDashboards(c.Req.Context(), c.OrgId, pluginID)
|
||||
if err != nil {
|
||||
var notFound plugins.NotFoundError
|
||||
if errors.As(err, ¬Found) {
|
||||
return response.Error(404, notFound.Error(), nil)
|
||||
}
|
||||
|
||||
return response.Error(500, "Failed to get plugin dashboards", err)
|
||||
}
|
||||
|
||||
return response.JSON(200, list)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response {
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
name := web.Params(c.Req)[":name"]
|
||||
|
@ -2,10 +2,10 @@ package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
)
|
||||
|
||||
@ -73,26 +73,32 @@ type PluginLoaderAuthorizer interface {
|
||||
CanLoadPlugin(plugin *Plugin) bool
|
||||
}
|
||||
|
||||
type PluginDashboardInfoDTO struct {
|
||||
UID string `json:"uid"`
|
||||
PluginId string `json:"pluginId"`
|
||||
Title string `json:"title"`
|
||||
Imported bool `json:"imported"`
|
||||
ImportedUri string `json:"importedUri"`
|
||||
ImportedUrl string `json:"importedUrl"`
|
||||
Slug string `json:"slug"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
FolderId int64 `json:"folderId"`
|
||||
ImportedRevision int64 `json:"importedRevision"`
|
||||
Revision int64 `json:"revision"`
|
||||
Description string `json:"description"`
|
||||
Path string `json:"path"`
|
||||
Removed bool `json:"removed"`
|
||||
// ListPluginDashboardFilesArgs list plugin dashboard files argument model.
|
||||
type ListPluginDashboardFilesArgs struct {
|
||||
PluginID string
|
||||
}
|
||||
|
||||
type PluginDashboardManager interface {
|
||||
// GetPluginDashboards gets dashboards for a certain org/plugin.
|
||||
GetPluginDashboards(ctx context.Context, orgID int64, pluginID string) ([]*PluginDashboardInfoDTO, error)
|
||||
// LoadPluginDashboard loads a plugin dashboard.
|
||||
LoadPluginDashboard(ctx context.Context, pluginID, path string) (*models.Dashboard, error)
|
||||
// GetPluginDashboardFilesArgs list plugin dashboard files result model.
|
||||
type ListPluginDashboardFilesResult struct {
|
||||
FileReferences []string
|
||||
}
|
||||
|
||||
// GetPluginDashboardFileContentsArgs get plugin dashboard file content argument model.
|
||||
type GetPluginDashboardFileContentsArgs struct {
|
||||
PluginID string
|
||||
FileReference string
|
||||
}
|
||||
|
||||
// GetPluginDashboardFileContentsResult get plugin dashboard file content result model.
|
||||
type GetPluginDashboardFileContentsResult struct {
|
||||
Content io.ReadCloser
|
||||
}
|
||||
|
||||
// DashboardFileStore is the interface for plugin dashboard file storage.
|
||||
type DashboardFileStore interface {
|
||||
// ListPluginDashboardFiles lists plugin dashboard files.
|
||||
ListPluginDashboardFiles(ctx context.Context, args *ListPluginDashboardFilesArgs) (*ListPluginDashboardFilesResult, error)
|
||||
|
||||
// GetPluginDashboardFileContents gets the referenced plugin dashboard file content.
|
||||
GetPluginDashboardFileContents(ctx context.Context, args *GetPluginDashboardFileContentsArgs) (*GetPluginDashboardFileContentsResult, error)
|
||||
}
|
||||
|
92
pkg/plugins/manager/dashboard_file_store.go
Normal file
92
pkg/plugins/manager/dashboard_file_store.go
Normal file
@ -0,0 +1,92 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var openDashboardFile = func(name string) (fs.File, error) {
|
||||
// Wrapping in filepath.Clean to properly handle
|
||||
// gosec G304 Potential file inclusion via variable rule.
|
||||
return os.Open(filepath.Clean(name))
|
||||
}
|
||||
|
||||
func (m *PluginManager) ListPluginDashboardFiles(ctx context.Context, args *plugins.ListPluginDashboardFilesArgs) (*plugins.ListPluginDashboardFilesResult, error) {
|
||||
if args == nil {
|
||||
return nil, fmt.Errorf("args cannot be nil")
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(args.PluginID)) == 0 {
|
||||
return nil, fmt.Errorf("args.PluginID cannot be empty")
|
||||
}
|
||||
|
||||
plugin, exists := m.Plugin(ctx, args.PluginID)
|
||||
if !exists {
|
||||
return nil, plugins.NotFoundError{PluginID: args.PluginID}
|
||||
}
|
||||
|
||||
references := []string{}
|
||||
for _, include := range plugin.DashboardIncludes() {
|
||||
references = append(references, include.Path)
|
||||
}
|
||||
|
||||
return &plugins.ListPluginDashboardFilesResult{
|
||||
FileReferences: references,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *PluginManager) GetPluginDashboardFileContents(ctx context.Context, args *plugins.GetPluginDashboardFileContentsArgs) (*plugins.GetPluginDashboardFileContentsResult, error) {
|
||||
if args == nil {
|
||||
return nil, fmt.Errorf("args cannot be nil")
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(args.PluginID)) == 0 {
|
||||
return nil, fmt.Errorf("args.PluginID cannot be empty")
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(args.FileReference)) == 0 {
|
||||
return nil, fmt.Errorf("args.FileReference cannot be empty")
|
||||
}
|
||||
|
||||
plugin, exists := m.Plugin(ctx, args.PluginID)
|
||||
if !exists {
|
||||
return nil, plugins.NotFoundError{PluginID: args.PluginID}
|
||||
}
|
||||
|
||||
var includedFile *plugins.Includes
|
||||
for _, include := range plugin.DashboardIncludes() {
|
||||
if args.FileReference == include.Path {
|
||||
includedFile = include
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if includedFile == nil {
|
||||
return nil, fmt.Errorf("plugin dashboard file not found")
|
||||
}
|
||||
|
||||
cleanPath, err := util.CleanRelativePath(includedFile.Path)
|
||||
if err != nil {
|
||||
// CleanRelativePath should clean and make the path relative so this is not expected to fail
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dashboardFilePath := filepath.Join(plugin.PluginDir, cleanPath)
|
||||
file, err := openDashboardFile(dashboardFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &plugins.GetPluginDashboardFileContentsResult{
|
||||
Content: file,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ plugins.DashboardFileStore = &PluginManager{}
|
223
pkg/plugins/manager/dashboard_file_store_test.go
Normal file
223
pkg/plugins/manager/dashboard_file_store_test.go
Normal file
@ -0,0 +1,223 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDashboardFileStore(t *testing.T) {
|
||||
m := setupPluginDashboardsForTest(t)
|
||||
|
||||
t.Run("Input validation", func(t *testing.T) {
|
||||
t.Run("ListPluginDashboardFiles", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args *plugins.ListPluginDashboardFilesArgs
|
||||
}{
|
||||
{
|
||||
name: "nil args should return error",
|
||||
},
|
||||
{
|
||||
name: "empty args.PluginID should return error",
|
||||
args: &plugins.ListPluginDashboardFilesArgs{},
|
||||
},
|
||||
{
|
||||
name: "args.PluginID with only space should return error",
|
||||
args: &plugins.ListPluginDashboardFilesArgs{PluginID: " \t "},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, err := m.ListPluginDashboardFiles(context.Background(), tc.args)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetPluginDashboardFileContents", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args *plugins.GetPluginDashboardFileContentsArgs
|
||||
}{
|
||||
{
|
||||
name: "nil args should return error",
|
||||
},
|
||||
{
|
||||
name: "empty args.PluginID should return error",
|
||||
args: &plugins.GetPluginDashboardFileContentsArgs{},
|
||||
},
|
||||
{
|
||||
name: "args.PluginID with only space should return error",
|
||||
args: &plugins.GetPluginDashboardFileContentsArgs{PluginID: " "},
|
||||
},
|
||||
{
|
||||
name: "empty args.FileReference should return error",
|
||||
args: &plugins.GetPluginDashboardFileContentsArgs{
|
||||
PluginID: "pluginWithDashboards",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "args.FileReference with only space should return error",
|
||||
args: &plugins.GetPluginDashboardFileContentsArgs{
|
||||
PluginID: "pluginWithDashboard",
|
||||
FileReference: " \t",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, err := m.GetPluginDashboardFileContents(context.Background(), tc.args)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Plugin without dashboards", func(t *testing.T) {
|
||||
t.Run("Should return zero file references", func(t *testing.T) {
|
||||
res, err := m.ListPluginDashboardFiles(context.Background(), &plugins.ListPluginDashboardFilesArgs{
|
||||
PluginID: "pluginWithoutDashboards",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, res)
|
||||
require.Len(t, res.FileReferences, 0)
|
||||
})
|
||||
|
||||
t.Run("Should return file not found error when trying to get non-existing plugin dashboard file content", func(t *testing.T) {
|
||||
res, err := m.GetPluginDashboardFileContents(context.Background(), &plugins.GetPluginDashboardFileContentsArgs{
|
||||
PluginID: "pluginWithoutDashboards",
|
||||
FileReference: "dashboards/dash2.json",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, "plugin dashboard file not found")
|
||||
require.Nil(t, res)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Plugin with dashboards", func(t *testing.T) {
|
||||
t.Run("Should return two file references", func(t *testing.T) {
|
||||
res, err := m.ListPluginDashboardFiles(context.Background(), &plugins.ListPluginDashboardFilesArgs{
|
||||
PluginID: "pluginWithDashboards",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, res)
|
||||
require.Len(t, res.FileReferences, 2)
|
||||
})
|
||||
|
||||
t.Run("With filesystem", func(t *testing.T) {
|
||||
origOpenDashboardFile := openDashboardFile
|
||||
mapFs := fstest.MapFS{
|
||||
"plugins/plugin-id/dashboards/dash1.json": {
|
||||
Data: []byte("dash1"),
|
||||
},
|
||||
"plugins/plugin-id/dashboards/dash2.json": {
|
||||
Data: []byte("dash2"),
|
||||
},
|
||||
"plugins/plugin-id/dashboards/dash3.json": {
|
||||
Data: []byte("dash3"),
|
||||
},
|
||||
"plugins/plugin-id/dash2.json": {
|
||||
Data: []byte("dash2"),
|
||||
},
|
||||
}
|
||||
openDashboardFile = mapFs.Open
|
||||
t.Cleanup(func() {
|
||||
openDashboardFile = origOpenDashboardFile
|
||||
})
|
||||
|
||||
t.Run("Should return file not found error when trying to get non-existing plugin dashboard file content", func(t *testing.T) {
|
||||
res, err := m.GetPluginDashboardFileContents(context.Background(), &plugins.GetPluginDashboardFileContentsArgs{
|
||||
PluginID: "pluginWithDashboards",
|
||||
FileReference: "dashboards/dash3.json",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, "plugin dashboard file not found")
|
||||
require.Nil(t, res)
|
||||
})
|
||||
|
||||
t.Run("Should return file content for dashboards/dash1.json", func(t *testing.T) {
|
||||
res, err := m.GetPluginDashboardFileContents(context.Background(), &plugins.GetPluginDashboardFileContentsArgs{
|
||||
PluginID: "pluginWithDashboards",
|
||||
FileReference: "dashboards/dash1.json",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, res)
|
||||
require.NotNil(t, res.Content)
|
||||
b, err := io.ReadAll(res.Content)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "dash1", string(b))
|
||||
require.NoError(t, res.Content.Close())
|
||||
})
|
||||
|
||||
t.Run("Should return file content for dashboards/dash2.json", func(t *testing.T) {
|
||||
res, err := m.GetPluginDashboardFileContents(context.Background(), &plugins.GetPluginDashboardFileContentsArgs{
|
||||
PluginID: "pluginWithDashboards",
|
||||
FileReference: "dashboards/dash2.json",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, res)
|
||||
require.NotNil(t, res.Content)
|
||||
b, err := io.ReadAll(res.Content)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "dash2", string(b))
|
||||
require.NoError(t, res.Content.Close())
|
||||
})
|
||||
|
||||
t.Run("Should return error when trying to read relative file", func(t *testing.T) {
|
||||
res, err := m.GetPluginDashboardFileContents(context.Background(), &plugins.GetPluginDashboardFileContentsArgs{
|
||||
PluginID: "pluginWithDashboards",
|
||||
FileReference: "dashboards/../dash2.json",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, "plugin dashboard file not found")
|
||||
require.Nil(t, res)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func setupPluginDashboardsForTest(t *testing.T) *PluginManager {
|
||||
t.Helper()
|
||||
|
||||
return &PluginManager{
|
||||
store: map[string]*plugins.Plugin{
|
||||
"pluginWithoutDashboards": {
|
||||
JSONData: plugins.JSONData{
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Type: "page",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"pluginWithDashboards": {
|
||||
PluginDir: "plugins/plugin-id",
|
||||
JSONData: plugins.JSONData{
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Type: "page",
|
||||
},
|
||||
{
|
||||
Type: "dashboard",
|
||||
Path: "dashboards/dash1.json",
|
||||
},
|
||||
{
|
||||
Type: "dashboard",
|
||||
Path: "dashboards/dash2.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func (m *PluginManager) GetPluginDashboards(ctx context.Context, orgID int64, pluginID string) ([]*plugins.PluginDashboardInfoDTO, error) {
|
||||
plugin, exists := m.Plugin(ctx, pluginID)
|
||||
if !exists {
|
||||
return nil, plugins.NotFoundError{PluginID: pluginID}
|
||||
}
|
||||
|
||||
result := make([]*plugins.PluginDashboardInfoDTO, 0)
|
||||
|
||||
// load current dashboards
|
||||
query := models.GetDashboardsByPluginIdQuery{OrgId: orgID, PluginId: pluginID}
|
||||
if err := bus.Dispatch(ctx, &query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingMatches := make(map[int64]bool)
|
||||
for _, include := range plugin.Includes {
|
||||
if include.Type != plugins.TypeDashboard {
|
||||
continue
|
||||
}
|
||||
|
||||
dashboard, err := m.LoadPluginDashboard(ctx, plugin.ID, include.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &plugins.PluginDashboardInfoDTO{}
|
||||
res.UID = dashboard.Uid
|
||||
res.Path = include.Path
|
||||
res.PluginId = plugin.ID
|
||||
res.Title = dashboard.Title
|
||||
res.Revision = dashboard.Data.Get("revision").MustInt64(1)
|
||||
|
||||
// find existing dashboard
|
||||
for _, existingDash := range query.Result {
|
||||
if existingDash.Slug == dashboard.Slug {
|
||||
res.UID = existingDash.Uid
|
||||
res.DashboardId = existingDash.Id
|
||||
res.Imported = true
|
||||
res.ImportedUri = "db/" + existingDash.Slug
|
||||
res.ImportedUrl = existingDash.GetUrl()
|
||||
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
|
||||
existingMatches[existingDash.Id] = true
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, res)
|
||||
}
|
||||
|
||||
// find deleted dashboards
|
||||
for _, dash := range query.Result {
|
||||
if _, exists := existingMatches[dash.Id]; !exists {
|
||||
result = append(result, &plugins.PluginDashboardInfoDTO{
|
||||
UID: dash.Uid,
|
||||
Slug: dash.Slug,
|
||||
DashboardId: dash.Id,
|
||||
Removed: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *PluginManager) LoadPluginDashboard(ctx context.Context, pluginID, path string) (*models.Dashboard, error) {
|
||||
if len(strings.TrimSpace(pluginID)) == 0 {
|
||||
return nil, fmt.Errorf("pluginID cannot be empty")
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(path)) == 0 {
|
||||
return nil, fmt.Errorf("path cannot be empty")
|
||||
}
|
||||
|
||||
plugin, exists := m.Plugin(ctx, pluginID)
|
||||
if !exists {
|
||||
return nil, plugins.NotFoundError{PluginID: pluginID}
|
||||
}
|
||||
|
||||
cleanPath, err := util.CleanRelativePath(path)
|
||||
if err != nil {
|
||||
// CleanRelativePath should clean and make the path relative so this is not expected to fail
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dashboardFilePath := filepath.Join(plugin.PluginDir, cleanPath)
|
||||
|
||||
included := false
|
||||
for _, include := range plugin.DashboardIncludes() {
|
||||
if filepath.Join(plugin.PluginDir, include.Path) == dashboardFilePath {
|
||||
included = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !included {
|
||||
return nil, fmt.Errorf("dashboard not included in plugin")
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
|
||||
// on plugin folder structure on disk and not user input. `path` input validation above
|
||||
// should only allow paths defined in the plugin's plugin.json.
|
||||
reader, err := os.Open(dashboardFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil {
|
||||
m.log.Warn("Failed to close file", "path", dashboardFilePath, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := simplejson.NewFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return models.NewDashboardFromJson(data), nil
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetPluginDashboards(t *testing.T) {
|
||||
cfg := &setting.Cfg{
|
||||
PluginSettings: setting.PluginSettings{
|
||||
"test-app": map[string]string{
|
||||
"path": "testdata/test-app",
|
||||
},
|
||||
},
|
||||
}
|
||||
pmCfg := plugins.FromGrafanaCfg(cfg)
|
||||
pm, err := ProvideService(cfg, loader.New(pmCfg, nil,
|
||||
signature.NewUnsignedAuthorizer(pmCfg), &provider.Service{}))
|
||||
require.NoError(t, err)
|
||||
|
||||
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardQuery) error {
|
||||
if query.Slug == "nginx-connections" {
|
||||
dash := models.NewDashboard("Nginx Connections")
|
||||
dash.Data.Set("revision", "1.1")
|
||||
query.Result = dash
|
||||
return nil
|
||||
}
|
||||
|
||||
return models.ErrDashboardNotFound
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error {
|
||||
var data = simplejson.New()
|
||||
data.Set("title", "Nginx Connections")
|
||||
data.Set("revision", 22)
|
||||
|
||||
query.Result = []*models.Dashboard{
|
||||
{Slug: "nginx-connections", Data: data},
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
dashboards, err := pm.GetPluginDashboards(context.Background(), 1, "test-app")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, dashboards, 2)
|
||||
require.Equal(t, "Nginx Connections", dashboards[0].Title)
|
||||
require.Equal(t, int64(25), dashboards[0].Revision)
|
||||
require.Equal(t, int64(22), dashboards[0].ImportedRevision)
|
||||
require.Equal(t, "db/nginx-connections", dashboards[0].ImportedUri)
|
||||
|
||||
require.Equal(t, int64(2), dashboards[1].Revision)
|
||||
require.Equal(t, int64(0), dashboards[1].ImportedRevision)
|
||||
}
|
@ -24,7 +24,6 @@ const (
|
||||
|
||||
var _ plugins.Client = (*PluginManager)(nil)
|
||||
var _ plugins.Store = (*PluginManager)(nil)
|
||||
var _ plugins.PluginDashboardManager = (*PluginManager)(nil)
|
||||
var _ plugins.StaticRouteResolver = (*PluginManager)(nil)
|
||||
var _ plugins.RendererManager = (*PluginManager)(nil)
|
||||
|
||||
|
@ -17,7 +17,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/live/pushhttp"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert"
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
@ -35,8 +35,9 @@ func ProvideBackgroundServiceRegistry(
|
||||
metrics *metrics.InternalMetricsService, secretsService *secretsManager.SecretsService,
|
||||
remoteCache *remotecache.RemoteCache, thumbnailsService thumbs.Service,
|
||||
// Need to make sure these are initialized, is there a better place to put them?
|
||||
_ *plugindashboards.Service, _ *dashboardsnapshots.Service,
|
||||
_ *alerting.AlertNotificationService, _ serviceaccounts.Service, _ *guardian.Provider,
|
||||
_ *dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
|
||||
_ serviceaccounts.Service, _ *guardian.Provider,
|
||||
_ *plugindashboardsservice.DashboardUpdater,
|
||||
) *BackgroundServiceRegistry {
|
||||
return NewBackgroundServiceRegistry(
|
||||
httpServer,
|
||||
|
@ -58,6 +58,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
||||
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service"
|
||||
"github.com/grafana/grafana/pkg/services/query"
|
||||
@ -126,8 +127,8 @@ var wireBasicSet = wire.NewSet(
|
||||
manager.ProvideService,
|
||||
wire.Bind(new(plugins.Client), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.Store), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.DashboardFileStore), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.StaticRouteResolver), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.PluginDashboardManager), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.RendererManager), new(*manager.PluginManager)),
|
||||
coreplugin.ProvideCoreRegistry,
|
||||
loader.ProvideService,
|
||||
@ -212,11 +213,14 @@ var wireBasicSet = wire.NewSet(
|
||||
dashboardstore.ProvideDashboardStore,
|
||||
wire.Bind(new(dashboards.DashboardService), new(*dashboardservice.DashboardServiceImpl)),
|
||||
wire.Bind(new(dashboards.DashboardProvisioningService), new(*dashboardservice.DashboardServiceImpl)),
|
||||
wire.Bind(new(dashboards.PluginService), new(*dashboardservice.DashboardServiceImpl)),
|
||||
wire.Bind(new(dashboards.FolderService), new(*dashboardservice.FolderServiceImpl)),
|
||||
wire.Bind(new(dashboards.Store), new(*dashboardstore.DashboardStore)),
|
||||
dashboardimportservice.ProvideService,
|
||||
wire.Bind(new(dashboardimport.Service), new(*dashboardimportservice.ImportDashboardService)),
|
||||
plugindashboards.ProvideService,
|
||||
plugindashboardsservice.ProvideService,
|
||||
wire.Bind(new(plugindashboards.Service), new(*plugindashboardsservice.Service)),
|
||||
plugindashboardsservice.ProvideDashboardUpdater,
|
||||
alerting.ProvideDashAlertExtractorService,
|
||||
wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)),
|
||||
comments.ProvideService,
|
||||
|
@ -43,9 +43,8 @@ func TestImportDashboardAPI(t *testing.T) {
|
||||
cmd := &dashboardimport.ImportDashboardRequest{}
|
||||
jsonBytes, err := json.Marshal(cmd)
|
||||
require.NoError(t, err)
|
||||
req := s.NewRequest(http.MethodPost, "/api/dashboards/import", bytes.NewReader(jsonBytes))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
resp, err := s.Send(req)
|
||||
req := s.NewPostRequest("/api/dashboards/import", bytes.NewReader(jsonBytes))
|
||||
resp, err := s.SendJSON(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
@ -58,12 +57,11 @@ func TestImportDashboardAPI(t *testing.T) {
|
||||
}
|
||||
jsonBytes, err := json.Marshal(cmd)
|
||||
require.NoError(t, err)
|
||||
req := s.NewRequest(http.MethodPost, "/api/dashboards/import", bytes.NewReader(jsonBytes))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req := s.NewPostRequest("/api/dashboards/import", bytes.NewReader(jsonBytes))
|
||||
webtest.RequestWithSignedInUser(req, &models.SignedInUser{
|
||||
UserId: 1,
|
||||
})
|
||||
resp, err := s.Send(req)
|
||||
resp, err := s.SendJSON(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode)
|
||||
@ -75,12 +73,11 @@ func TestImportDashboardAPI(t *testing.T) {
|
||||
}
|
||||
jsonBytes, err := json.Marshal(cmd)
|
||||
require.NoError(t, err)
|
||||
req := s.NewRequest(http.MethodPost, "/api/dashboards/import", bytes.NewReader(jsonBytes))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req := s.NewPostRequest("/api/dashboards/import", bytes.NewReader(jsonBytes))
|
||||
webtest.RequestWithSignedInUser(req, &models.SignedInUser{
|
||||
UserId: 1,
|
||||
})
|
||||
resp, err := s.Send(req)
|
||||
resp, err := s.SendJSON(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
@ -93,12 +90,11 @@ func TestImportDashboardAPI(t *testing.T) {
|
||||
}
|
||||
jsonBytes, err := json.Marshal(cmd)
|
||||
require.NoError(t, err)
|
||||
req := s.NewRequest(http.MethodPost, "/api/dashboards/import?trimdefaults=true", bytes.NewReader(jsonBytes))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req := s.NewPostRequest("/api/dashboards/import?trimdefaults=true", bytes.NewReader(jsonBytes))
|
||||
webtest.RequestWithSignedInUser(req, &models.SignedInUser{
|
||||
UserId: 1,
|
||||
})
|
||||
resp, err := s.Send(req)
|
||||
resp, err := s.SendJSON(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
@ -136,12 +132,11 @@ func TestImportDashboardAPI(t *testing.T) {
|
||||
}
|
||||
jsonBytes, err := json.Marshal(cmd)
|
||||
require.NoError(t, err)
|
||||
req := s.NewRequest(http.MethodPost, "/api/dashboards/import?trimdefaults=true", bytes.NewReader(jsonBytes))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req := s.NewPostRequest("/api/dashboards/import?trimdefaults=true", bytes.NewReader(jsonBytes))
|
||||
webtest.RequestWithSignedInUser(req, &models.SignedInUser{
|
||||
UserId: 1,
|
||||
})
|
||||
resp, err := s.Send(req)
|
||||
resp, err := s.SendJSON(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
@ -165,12 +160,11 @@ func TestImportDashboardAPI(t *testing.T) {
|
||||
}
|
||||
jsonBytes, err := json.Marshal(cmd)
|
||||
require.NoError(t, err)
|
||||
req := s.NewRequest(http.MethodPost, "/api/dashboards/import", bytes.NewReader(jsonBytes))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req := s.NewPostRequest("/api/dashboards/import", bytes.NewReader(jsonBytes))
|
||||
webtest.RequestWithSignedInUser(req, &models.SignedInUser{
|
||||
UserId: 1,
|
||||
})
|
||||
resp, err := s.Send(req)
|
||||
resp, err := s.SendJSON(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
@ -12,18 +12,19 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboardimport/utils"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/schemaloader"
|
||||
)
|
||||
|
||||
func ProvideService(routeRegister routing.RouteRegister,
|
||||
quotaService *quota.QuotaService, schemaLoaderService *schemaloader.SchemaLoaderService,
|
||||
pluginDashboardManager plugins.PluginDashboardManager, pluginStore plugins.Store,
|
||||
pluginDashboardService plugindashboards.Service, pluginStore plugins.Store,
|
||||
libraryPanelService librarypanels.Service, dashboardService dashboards.DashboardService,
|
||||
ac accesscontrol.AccessControl,
|
||||
) *ImportDashboardService {
|
||||
s := &ImportDashboardService{
|
||||
pluginDashboardManager: pluginDashboardManager,
|
||||
pluginDashboardService: pluginDashboardService,
|
||||
dashboardService: dashboardService,
|
||||
libraryPanelService: libraryPanelService,
|
||||
}
|
||||
@ -35,7 +36,7 @@ func ProvideService(routeRegister routing.RouteRegister,
|
||||
}
|
||||
|
||||
type ImportDashboardService struct {
|
||||
pluginDashboardManager plugins.PluginDashboardManager
|
||||
pluginDashboardService plugindashboards.Service
|
||||
dashboardService dashboards.DashboardService
|
||||
libraryPanelService librarypanels.Service
|
||||
}
|
||||
@ -43,9 +44,14 @@ type ImportDashboardService struct {
|
||||
func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
|
||||
var dashboard *models.Dashboard
|
||||
if req.PluginId != "" {
|
||||
var err error
|
||||
if dashboard, err = s.pluginDashboardManager.LoadPluginDashboard(ctx, req.PluginId, req.Path); err != nil {
|
||||
loadReq := &plugindashboards.LoadPluginDashboardRequest{
|
||||
PluginID: req.PluginId,
|
||||
Reference: req.Path,
|
||||
}
|
||||
if resp, err := s.pluginDashboardService.LoadPluginDashboard(ctx, loadReq); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
dashboard = resp.Dashboard
|
||||
}
|
||||
} else {
|
||||
dashboard = models.NewDashboardFromJson(req.Dashboard)
|
||||
|
@ -8,16 +8,16 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardimport"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestImportDashboardService(t *testing.T) {
|
||||
t.Run("When importing a plugin dashboard should save dashboard and sync library panels", func(t *testing.T) {
|
||||
pluginDashboardManager := &pluginDashboardManagerMock{
|
||||
pluginDashboardService := &pluginDashboardServiceMock{
|
||||
loadPluginDashboardFunc: loadTestDashboard,
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ func TestImportDashboardService(t *testing.T) {
|
||||
},
|
||||
}
|
||||
s := &ImportDashboardService{
|
||||
pluginDashboardManager: pluginDashboardManager,
|
||||
pluginDashboardService: pluginDashboardService,
|
||||
dashboardService: dashboardService,
|
||||
libraryPanelService: libraryPanelService,
|
||||
}
|
||||
@ -108,11 +108,14 @@ func TestImportDashboardService(t *testing.T) {
|
||||
libraryPanelService: libraryPanelService,
|
||||
}
|
||||
|
||||
dash, err := loadTestDashboard(context.Background(), "", "dashboard.json")
|
||||
loadResp, err := loadTestDashboard(context.Background(), &plugindashboards.LoadPluginDashboardRequest{
|
||||
PluginID: "",
|
||||
Reference: "dashboard.json",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dashboardimport.ImportDashboardRequest{
|
||||
Dashboard: dash.Data,
|
||||
Dashboard: loadResp.Dashboard.Data,
|
||||
Path: "plugin_dashboard.json",
|
||||
Inputs: []dashboardimport.ImportDashboardInput{
|
||||
{Name: "*", Type: "datasource", Value: "prom"},
|
||||
@ -136,10 +139,10 @@ func TestImportDashboardService(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func loadTestDashboard(ctx context.Context, pluginID, path string) (*models.Dashboard, error) {
|
||||
func loadTestDashboard(ctx context.Context, req *plugindashboards.LoadPluginDashboardRequest) (*plugindashboards.LoadPluginDashboardResponse, error) {
|
||||
// It's safe to ignore gosec warning G304 since this is a test and arguments comes from test configuration.
|
||||
// nolint:gosec
|
||||
bytes, err := ioutil.ReadFile(filepath.Join("testdata", path))
|
||||
bytes, err := ioutil.ReadFile(filepath.Join("testdata", req.Reference))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -149,17 +152,19 @@ func loadTestDashboard(ctx context.Context, pluginID, path string) (*models.Dash
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return models.NewDashboardFromJson(dashboardJSON), nil
|
||||
return &plugindashboards.LoadPluginDashboardResponse{
|
||||
Dashboard: models.NewDashboardFromJson(dashboardJSON),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type pluginDashboardManagerMock struct {
|
||||
plugins.PluginDashboardManager
|
||||
loadPluginDashboardFunc func(ctx context.Context, pluginID, path string) (*models.Dashboard, error)
|
||||
type pluginDashboardServiceMock struct {
|
||||
plugindashboards.Service
|
||||
loadPluginDashboardFunc func(ctx context.Context, req *plugindashboards.LoadPluginDashboardRequest) (*plugindashboards.LoadPluginDashboardResponse, error)
|
||||
}
|
||||
|
||||
func (m *pluginDashboardManagerMock) LoadPluginDashboard(ctx context.Context, pluginID, path string) (*models.Dashboard, error) {
|
||||
func (m *pluginDashboardServiceMock) LoadPluginDashboard(ctx context.Context, req *plugindashboards.LoadPluginDashboardRequest) (*plugindashboards.LoadPluginDashboardResponse, error) {
|
||||
if m.loadPluginDashboardFunc != nil {
|
||||
return m.loadPluginDashboardFunc(ctx, pluginID, path)
|
||||
return m.loadPluginDashboardFunc(ctx, req)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
@ -6,6 +6,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
//go:generate mockery --name Store --structname FakeDashboardStore --inpackage --filename database_mock.go
|
||||
|
||||
// DashboardService is a service for operating on dashboards.
|
||||
type DashboardService interface {
|
||||
SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error)
|
||||
@ -16,6 +18,11 @@ type DashboardService interface {
|
||||
UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error
|
||||
}
|
||||
|
||||
// PluginService is a service for operating on plugin dashboards.
|
||||
type PluginService interface {
|
||||
GetDashboardsByPluginID(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error
|
||||
}
|
||||
|
||||
//go:generate mockery --name DashboardProvisioningService --structname FakeDashboardProvisioning --inpackage --filename dashboard_provisioning_mock.go
|
||||
// DashboardProvisioningService is a service for operating on provisioned dashboards.
|
||||
type DashboardProvisioningService interface {
|
||||
@ -29,7 +36,6 @@ type DashboardProvisioningService interface {
|
||||
DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error
|
||||
}
|
||||
|
||||
//go:generate mockery --name Store --structname FakeDashboardStore --inpackage --filename database_mock.go
|
||||
// Store is a dashboard store.
|
||||
type Store interface {
|
||||
// ValidateDashboardBeforeSave validates a dashboard before save.
|
||||
@ -46,4 +52,6 @@ type Store interface {
|
||||
// SaveAlerts saves dashboard alerts.
|
||||
SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error
|
||||
UnprovisionDashboard(ctx context.Context, id int64) error
|
||||
// GetDashboardsByPluginID retrieves dashboards identified by plugin.
|
||||
GetDashboardsByPluginID(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error
|
||||
}
|
||||
|
@ -635,3 +635,14 @@ func EnsureTagsExist(sess *sqlstore.DBSession, tags []*models.Tag) ([]*models.Ta
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (d *DashboardStore) GetDashboardsByPluginID(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error {
|
||||
return d.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
var dashboards = make([]*models.Dashboard, 0)
|
||||
whereExpr := "org_id=? AND plugin_id=? AND is_folder=" + d.sqlStore.Dialect.BooleanStr(false)
|
||||
|
||||
err := dbSession.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards)
|
||||
query.Result = dashboards
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
@ -421,7 +421,7 @@ func TestDashboardDataAccessGivenPluginWithImportedDashboards(t *testing.T) {
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
err := sqlStore.GetDashboardsByPluginId(context.Background(), &query)
|
||||
err := dashboardStore.GetDashboardsByPluginID(context.Background(), &query)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(query.Result), 2)
|
||||
}
|
||||
|
@ -5,9 +5,8 @@ package dashboards
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
models "github.com/grafana/grafana/pkg/models"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// FakeDashboardStore is an autogenerated mock type for the Store type
|
||||
@ -29,6 +28,20 @@ func (_m *FakeDashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Co
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetDashboardsByPluginID provides a mock function with given fields: ctx, query
|
||||
func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardsByPluginIdQuery) error); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetFolderByTitle provides a mock function with given fields: orgID, title
|
||||
func (_m *FakeDashboardStore) GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error) {
|
||||
ret := _m.Called(orgID, title)
|
||||
|
@ -443,6 +443,10 @@ func (dr *DashboardServiceImpl) UnprovisionDashboard(ctx context.Context, dashbo
|
||||
return dr.dashboardStore.UnprovisionDashboard(ctx, dashboardId)
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error {
|
||||
return dr.dashboardStore.GetDashboardsByPluginID(ctx, query)
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *m.SaveDashboardDTO, dash *models.Dashboard, provisioned bool) error {
|
||||
inFolder := dash.FolderId > 0
|
||||
if dr.features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
|
@ -2,166 +2,55 @@ package plugindashboards
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardimport"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
||||
)
|
||||
|
||||
func ProvideService(bus bus.Bus, pluginStore plugins.Store, pluginDashboardManager plugins.PluginDashboardManager,
|
||||
dashboardImportService dashboardimport.Service, pluginSettingsService pluginsettings.Service) *Service {
|
||||
s := newService(bus, pluginStore, pluginDashboardManager, dashboardImportService, pluginSettingsService)
|
||||
s.updateAppDashboards()
|
||||
return s
|
||||
// PluginDashboard plugin dashboard model..
|
||||
type PluginDashboard struct {
|
||||
UID string `json:"uid"`
|
||||
PluginId string `json:"pluginId"`
|
||||
Title string `json:"title"`
|
||||
Imported bool `json:"imported"`
|
||||
ImportedUri string `json:"importedUri"`
|
||||
ImportedUrl string `json:"importedUrl"`
|
||||
Slug string `json:"slug"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
FolderId int64 `json:"folderId"`
|
||||
ImportedRevision int64 `json:"importedRevision"`
|
||||
Revision int64 `json:"revision"`
|
||||
Description string `json:"description"`
|
||||
Reference string `json:"path"`
|
||||
Removed bool `json:"removed"`
|
||||
}
|
||||
|
||||
func newService(bus bus.Bus, pluginStore plugins.Store, pluginDashboardManager plugins.PluginDashboardManager,
|
||||
dashboardImportService dashboardimport.Service, pluginSettingsService pluginsettings.Service) *Service {
|
||||
s := &Service{
|
||||
bus: bus,
|
||||
pluginStore: pluginStore,
|
||||
pluginDashboardManager: pluginDashboardManager,
|
||||
dashboardImportService: dashboardImportService,
|
||||
pluginSettingsService: pluginSettingsService,
|
||||
logger: log.New("plugindashboards"),
|
||||
}
|
||||
bus.AddEventListener(s.handlePluginStateChanged)
|
||||
|
||||
return s
|
||||
// ListPluginDashboardsRequest request object for listing plugin dashboards.
|
||||
type ListPluginDashboardsRequest struct {
|
||||
OrgID int64
|
||||
PluginID string
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
bus bus.Bus
|
||||
pluginStore plugins.Store
|
||||
pluginDashboardManager plugins.PluginDashboardManager
|
||||
dashboardImportService dashboardimport.Service
|
||||
pluginSettingsService pluginsettings.Service
|
||||
logger log.Logger
|
||||
// ListPluginDashboardsResponse response object for listing plugin dashboards.
|
||||
type ListPluginDashboardsResponse struct {
|
||||
Items []*PluginDashboard
|
||||
}
|
||||
|
||||
func (s *Service) updateAppDashboards() {
|
||||
s.logger.Debug("Looking for app dashboard updates")
|
||||
|
||||
pluginSettings, err := s.pluginSettingsService.GetPluginSettings(context.Background(), 0)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get all plugin settings", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, pluginSetting := range pluginSettings {
|
||||
// ignore disabled plugins
|
||||
if !pluginSetting.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if pluginDef, exists := s.pluginStore.Plugin(context.Background(), pluginSetting.PluginId); exists {
|
||||
if pluginDef.Info.Version != pluginSetting.PluginVersion {
|
||||
s.syncPluginDashboards(context.Background(), pluginDef, pluginSetting.OrgId)
|
||||
}
|
||||
}
|
||||
}
|
||||
// LoadPluginDashboardRequest request object for loading a plugin dashboard.
|
||||
type LoadPluginDashboardRequest struct {
|
||||
PluginID string
|
||||
Reference string
|
||||
}
|
||||
|
||||
func (s *Service) syncPluginDashboards(ctx context.Context, plugin plugins.PluginDTO, orgID int64) {
|
||||
s.logger.Info("Syncing plugin dashboards to DB", "pluginId", plugin.ID)
|
||||
|
||||
// Get plugin dashboards
|
||||
dashboards, err := s.pluginDashboardManager.GetPluginDashboards(ctx, orgID, plugin.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to load app dashboards", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update dashboards with updated revisions
|
||||
for _, dash := range dashboards {
|
||||
// remove removed ones
|
||||
if dash.Removed {
|
||||
s.logger.Info("Deleting plugin dashboard", "pluginId", plugin.ID, "dashboard", dash.Slug)
|
||||
|
||||
deleteCmd := models.DeleteDashboardCommand{OrgId: orgID, Id: dash.DashboardId}
|
||||
if err := s.bus.Dispatch(ctx, &deleteCmd); err != nil {
|
||||
s.logger.Error("Failed to auto update app dashboard", "pluginId", plugin.ID, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// update updated ones
|
||||
if dash.ImportedRevision != dash.Revision {
|
||||
if err := s.autoUpdateAppDashboard(ctx, dash, orgID); err != nil {
|
||||
s.logger.Error("Failed to auto update app dashboard", "pluginId", plugin.ID, "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update version in plugin_setting table to mark that we have processed the update
|
||||
query := models.GetPluginSettingByIdQuery{PluginId: plugin.ID, OrgId: orgID}
|
||||
if err := s.pluginSettingsService.GetPluginSettingById(ctx, &query); err != nil {
|
||||
s.logger.Error("Failed to read plugin setting by ID", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
appSetting := query.Result
|
||||
cmd := models.UpdatePluginSettingVersionCmd{
|
||||
OrgId: appSetting.OrgId,
|
||||
PluginId: appSetting.PluginId,
|
||||
PluginVersion: plugin.Info.Version,
|
||||
}
|
||||
|
||||
if err := s.pluginSettingsService.UpdatePluginSettingVersion(ctx, &cmd); err != nil {
|
||||
s.logger.Error("Failed to update plugin setting version", "error", err)
|
||||
}
|
||||
// LoadPluginDashboardResponse response object for loading a plugin dashboard.
|
||||
type LoadPluginDashboardResponse struct {
|
||||
Dashboard *models.Dashboard
|
||||
}
|
||||
|
||||
func (s *Service) handlePluginStateChanged(ctx context.Context, event *models.PluginStateChangedEvent) error {
|
||||
s.logger.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled)
|
||||
// Service interface for listing plugin dashboards.
|
||||
type Service interface {
|
||||
// ListPluginDashboards list plugin dashboards identified by org/plugin.
|
||||
ListPluginDashboards(ctx context.Context, req *ListPluginDashboardsRequest) (*ListPluginDashboardsResponse, error)
|
||||
|
||||
if event.Enabled {
|
||||
p, exists := s.pluginStore.Plugin(ctx, event.PluginId)
|
||||
if !exists {
|
||||
return fmt.Errorf("plugin %s not found. Could not sync plugin dashboards", event.PluginId)
|
||||
}
|
||||
|
||||
s.syncPluginDashboards(ctx, p, event.OrgId)
|
||||
} else {
|
||||
query := models.GetDashboardsByPluginIdQuery{PluginId: event.PluginId, OrgId: event.OrgId}
|
||||
if err := s.bus.Dispatch(ctx, &query); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, dash := range query.Result {
|
||||
s.logger.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
|
||||
deleteCmd := models.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id}
|
||||
if err := s.bus.Dispatch(ctx, &deleteCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) autoUpdateAppDashboard(ctx context.Context, pluginDashInfo *plugins.PluginDashboardInfoDTO, orgID int64) error {
|
||||
dash, err := s.pluginDashboardManager.LoadPluginDashboard(ctx, pluginDashInfo.PluginId, pluginDashInfo.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logger.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev",
|
||||
pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision)
|
||||
_, err = s.dashboardImportService.ImportDashboard(ctx, &dashboardimport.ImportDashboardRequest{
|
||||
PluginId: pluginDashInfo.PluginId,
|
||||
User: &models.SignedInUser{UserId: 0, OrgRole: models.ROLE_ADMIN, OrgId: orgID},
|
||||
Path: pluginDashInfo.Path,
|
||||
FolderId: 0,
|
||||
Dashboard: dash.Data,
|
||||
Overwrite: true,
|
||||
Inputs: nil,
|
||||
})
|
||||
return err
|
||||
// LoadPluginDashboard loads a plugin dashboard identified by plugin and reference.
|
||||
LoadPluginDashboard(ctx context.Context, req *LoadPluginDashboardRequest) (*LoadPluginDashboardResponse, error)
|
||||
}
|
||||
|
181
pkg/services/plugindashboards/service/dashboard_updater.go
Normal file
181
pkg/services/plugindashboards/service/dashboard_updater.go
Normal file
@ -0,0 +1,181 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardimport"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
||||
)
|
||||
|
||||
func ProvideDashboardUpdater(bus bus.Bus, pluginStore plugins.Store, pluginDashboardService plugindashboards.Service,
|
||||
dashboardImportService dashboardimport.Service, pluginSettingsService pluginsettings.Service,
|
||||
dashboardPluginService dashboards.PluginService, dashboardService dashboards.DashboardService) *DashboardUpdater {
|
||||
du := newDashboardUpdater(bus, pluginStore, pluginDashboardService, dashboardImportService,
|
||||
pluginSettingsService, dashboardPluginService, dashboardService)
|
||||
du.updateAppDashboards()
|
||||
return du
|
||||
}
|
||||
|
||||
func newDashboardUpdater(bus bus.Bus, pluginStore plugins.Store,
|
||||
pluginDashboardService plugindashboards.Service, dashboardImportService dashboardimport.Service,
|
||||
pluginSettingsService pluginsettings.Service, dashboardPluginService dashboards.PluginService,
|
||||
dashboardService dashboards.DashboardService) *DashboardUpdater {
|
||||
s := &DashboardUpdater{
|
||||
pluginStore: pluginStore,
|
||||
pluginDashboardService: pluginDashboardService,
|
||||
dashboardImportService: dashboardImportService,
|
||||
pluginSettingsService: pluginSettingsService,
|
||||
dashboardPluginService: dashboardPluginService,
|
||||
dashboardService: dashboardService,
|
||||
logger: log.New("plugindashboards"),
|
||||
}
|
||||
bus.AddEventListener(s.handlePluginStateChanged)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
type DashboardUpdater struct {
|
||||
pluginStore plugins.Store
|
||||
pluginDashboardService plugindashboards.Service
|
||||
dashboardImportService dashboardimport.Service
|
||||
pluginSettingsService pluginsettings.Service
|
||||
dashboardPluginService dashboards.PluginService
|
||||
dashboardService dashboards.DashboardService
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (du *DashboardUpdater) updateAppDashboards() {
|
||||
du.logger.Debug("Looking for app dashboard updates")
|
||||
|
||||
pluginSettings, err := du.pluginSettingsService.GetPluginSettings(context.Background(), 0)
|
||||
if err != nil {
|
||||
du.logger.Error("Failed to get all plugin settings", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, pluginSetting := range pluginSettings {
|
||||
// ignore disabled plugins
|
||||
if !pluginSetting.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if pluginDef, exists := du.pluginStore.Plugin(context.Background(), pluginSetting.PluginId); exists {
|
||||
if pluginDef.Info.Version != pluginSetting.PluginVersion {
|
||||
du.syncPluginDashboards(context.Background(), pluginDef, pluginSetting.OrgId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (du *DashboardUpdater) syncPluginDashboards(ctx context.Context, plugin plugins.PluginDTO, orgID int64) {
|
||||
du.logger.Info("Syncing plugin dashboards to DB", "pluginId", plugin.ID)
|
||||
|
||||
// Get plugin dashboards
|
||||
req := &plugindashboards.ListPluginDashboardsRequest{
|
||||
OrgID: orgID,
|
||||
PluginID: plugin.ID,
|
||||
}
|
||||
resp, err := du.pluginDashboardService.ListPluginDashboards(ctx, req)
|
||||
if err != nil {
|
||||
du.logger.Error("Failed to load app dashboards", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update dashboards with updated revisions
|
||||
for _, dash := range resp.Items {
|
||||
// remove removed ones
|
||||
if dash.Removed {
|
||||
du.logger.Info("Deleting plugin dashboard", "pluginId", plugin.ID, "dashboard", dash.Slug)
|
||||
|
||||
if err := du.dashboardService.DeleteDashboard(ctx, dash.DashboardId, orgID); err != nil {
|
||||
du.logger.Error("Failed to auto update app dashboard", "pluginId", plugin.ID, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// update updated ones
|
||||
if dash.ImportedRevision != dash.Revision {
|
||||
if err := du.autoUpdateAppDashboard(ctx, dash, orgID); err != nil {
|
||||
du.logger.Error("Failed to auto update app dashboard", "pluginId", plugin.ID, "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update version in plugin_setting table to mark that we have processed the update
|
||||
query := models.GetPluginSettingByIdQuery{PluginId: plugin.ID, OrgId: orgID}
|
||||
if err := du.pluginSettingsService.GetPluginSettingById(ctx, &query); err != nil {
|
||||
du.logger.Error("Failed to read plugin setting by ID", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
appSetting := query.Result
|
||||
cmd := models.UpdatePluginSettingVersionCmd{
|
||||
OrgId: appSetting.OrgId,
|
||||
PluginId: appSetting.PluginId,
|
||||
PluginVersion: plugin.Info.Version,
|
||||
}
|
||||
|
||||
if err := du.pluginSettingsService.UpdatePluginSettingVersion(ctx, &cmd); err != nil {
|
||||
du.logger.Error("Failed to update plugin setting version", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (du *DashboardUpdater) handlePluginStateChanged(ctx context.Context, event *models.PluginStateChangedEvent) error {
|
||||
du.logger.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled)
|
||||
|
||||
if event.Enabled {
|
||||
p, exists := du.pluginStore.Plugin(ctx, event.PluginId)
|
||||
if !exists {
|
||||
return fmt.Errorf("plugin %s not found. Could not sync plugin dashboards", event.PluginId)
|
||||
}
|
||||
|
||||
du.syncPluginDashboards(ctx, p, event.OrgId)
|
||||
} else {
|
||||
query := models.GetDashboardsByPluginIdQuery{PluginId: event.PluginId, OrgId: event.OrgId}
|
||||
if err := du.dashboardPluginService.GetDashboardsByPluginID(ctx, &query); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, dash := range query.Result {
|
||||
du.logger.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
|
||||
if err := du.dashboardService.DeleteDashboard(ctx, dash.Id, dash.OrgId); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (du *DashboardUpdater) autoUpdateAppDashboard(ctx context.Context, pluginDashInfo *plugindashboards.PluginDashboard, orgID int64) error {
|
||||
req := &plugindashboards.LoadPluginDashboardRequest{
|
||||
PluginID: pluginDashInfo.PluginId,
|
||||
Reference: pluginDashInfo.Reference,
|
||||
}
|
||||
resp, err := du.pluginDashboardService.LoadPluginDashboard(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
du.logger.Info("Auto updating App dashboard", "dashboard", resp.Dashboard.Title, "newRev",
|
||||
pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision)
|
||||
_, err = du.dashboardImportService.ImportDashboard(ctx, &dashboardimport.ImportDashboardRequest{
|
||||
PluginId: pluginDashInfo.PluginId,
|
||||
User: &models.SignedInUser{UserId: 0, OrgRole: models.ROLE_ADMIN, OrgId: orgID},
|
||||
Path: pluginDashInfo.Reference,
|
||||
FolderId: 0,
|
||||
Dashboard: resp.Dashboard.Data,
|
||||
Overwrite: true,
|
||||
Inputs: nil,
|
||||
})
|
||||
return err
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package plugindashboards
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -9,19 +9,21 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardimport"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsettings/service"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
func TestDashboardUpdater(t *testing.T) {
|
||||
t.Run("updateAppDashboards", func(t *testing.T) {
|
||||
scenario(t, "Without any stored plugin settings shouldn't delete/import any dashboards",
|
||||
scenarioInput{}, func(ctx *scenarioContext) {
|
||||
ctx.s.updateAppDashboards()
|
||||
ctx.dashboardUpdater.updateAppDashboards()
|
||||
|
||||
require.Len(t, ctx.pluginSettingsService.getPluginSettingsArgs, 1)
|
||||
require.Equal(t, int64(0), ctx.pluginSettingsService.getPluginSettingsArgs[0])
|
||||
require.Empty(t, ctx.deleteDashboardArgs)
|
||||
require.Empty(t, ctx.dashboardService.deleteDashboardArgs)
|
||||
require.Empty(t, ctx.importDashboardArgs)
|
||||
})
|
||||
|
||||
@ -33,17 +35,17 @@ func TestService(t *testing.T) {
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
pluginDashboards: []*plugins.PluginDashboardInfoDTO{
|
||||
pluginDashboards: []*plugindashboards.PluginDashboard{
|
||||
{
|
||||
PluginId: "test",
|
||||
Path: "dashboard.json",
|
||||
PluginId: "test",
|
||||
Reference: "dashboard.json",
|
||||
},
|
||||
},
|
||||
}, func(ctx *scenarioContext) {
|
||||
ctx.s.updateAppDashboards()
|
||||
ctx.dashboardUpdater.updateAppDashboards()
|
||||
|
||||
require.NotEmpty(t, ctx.pluginSettingsService.getPluginSettingsArgs)
|
||||
require.Empty(t, ctx.deleteDashboardArgs)
|
||||
require.Empty(t, ctx.dashboardService.deleteDashboardArgs)
|
||||
require.Empty(t, ctx.importDashboardArgs)
|
||||
})
|
||||
|
||||
@ -55,17 +57,17 @@ func TestService(t *testing.T) {
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
pluginDashboards: []*plugins.PluginDashboardInfoDTO{
|
||||
pluginDashboards: []*plugindashboards.PluginDashboard{
|
||||
{
|
||||
PluginId: "test",
|
||||
Path: "dashboard.json",
|
||||
PluginId: "test",
|
||||
Reference: "dashboard.json",
|
||||
},
|
||||
},
|
||||
}, func(ctx *scenarioContext) {
|
||||
ctx.s.updateAppDashboards()
|
||||
ctx.dashboardUpdater.updateAppDashboards()
|
||||
|
||||
require.NotEmpty(t, ctx.pluginSettingsService.getPluginSettingsArgs)
|
||||
require.Empty(t, ctx.deleteDashboardArgs)
|
||||
require.Empty(t, ctx.dashboardService.deleteDashboardArgs)
|
||||
require.Empty(t, ctx.importDashboardArgs)
|
||||
})
|
||||
|
||||
@ -87,17 +89,17 @@ func TestService(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginDashboards: []*plugins.PluginDashboardInfoDTO{
|
||||
pluginDashboards: []*plugindashboards.PluginDashboard{
|
||||
{
|
||||
PluginId: "test",
|
||||
Path: "dashboard.json",
|
||||
PluginId: "test",
|
||||
Reference: "dashboard.json",
|
||||
},
|
||||
},
|
||||
}, func(ctx *scenarioContext) {
|
||||
ctx.s.updateAppDashboards()
|
||||
ctx.dashboardUpdater.updateAppDashboards()
|
||||
|
||||
require.NotEmpty(t, ctx.pluginSettingsService.getPluginSettingsArgs)
|
||||
require.Empty(t, ctx.deleteDashboardArgs)
|
||||
require.Empty(t, ctx.dashboardService.deleteDashboardArgs)
|
||||
require.Empty(t, ctx.importDashboardArgs)
|
||||
})
|
||||
|
||||
@ -119,20 +121,20 @@ func TestService(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginDashboards: []*plugins.PluginDashboardInfoDTO{
|
||||
pluginDashboards: []*plugindashboards.PluginDashboard{
|
||||
{
|
||||
PluginId: "test",
|
||||
Path: "dashboard.json",
|
||||
Reference: "dashboard.json",
|
||||
Removed: false,
|
||||
Revision: 1,
|
||||
ImportedRevision: 1,
|
||||
},
|
||||
},
|
||||
}, func(ctx *scenarioContext) {
|
||||
ctx.s.updateAppDashboards()
|
||||
ctx.dashboardUpdater.updateAppDashboards()
|
||||
|
||||
require.NotEmpty(t, ctx.pluginSettingsService.getPluginSettingsArgs)
|
||||
require.Empty(t, ctx.deleteDashboardArgs)
|
||||
require.Empty(t, ctx.dashboardService.deleteDashboardArgs)
|
||||
require.Empty(t, ctx.importDashboardArgs)
|
||||
})
|
||||
|
||||
@ -156,33 +158,33 @@ func TestService(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginDashboards: []*plugins.PluginDashboardInfoDTO{
|
||||
pluginDashboards: []*plugindashboards.PluginDashboard{
|
||||
{
|
||||
DashboardId: 3,
|
||||
PluginId: "test",
|
||||
Path: "removed.json",
|
||||
Reference: "removed.json",
|
||||
Removed: true,
|
||||
},
|
||||
{
|
||||
DashboardId: 4,
|
||||
PluginId: "test",
|
||||
Path: "not-updated.json",
|
||||
Reference: "not-updated.json",
|
||||
},
|
||||
{
|
||||
DashboardId: 5,
|
||||
PluginId: "test",
|
||||
Path: "updated.json",
|
||||
Reference: "updated.json",
|
||||
Revision: 1,
|
||||
ImportedRevision: 2,
|
||||
},
|
||||
},
|
||||
}, func(ctx *scenarioContext) {
|
||||
ctx.s.updateAppDashboards()
|
||||
ctx.dashboardUpdater.updateAppDashboards()
|
||||
|
||||
require.NotEmpty(t, ctx.pluginSettingsService.getPluginSettingsArgs)
|
||||
require.Len(t, ctx.deleteDashboardArgs, 1)
|
||||
require.Equal(t, int64(2), ctx.deleteDashboardArgs[0].OrgId)
|
||||
require.Equal(t, int64(3), ctx.deleteDashboardArgs[0].Id)
|
||||
require.Len(t, ctx.dashboardService.deleteDashboardArgs, 1)
|
||||
require.Equal(t, int64(2), ctx.dashboardService.deleteDashboardArgs[0].orgId)
|
||||
require.Equal(t, int64(3), ctx.dashboardService.deleteDashboardArgs[0].dashboardId)
|
||||
|
||||
require.Len(t, ctx.importDashboardArgs, 1)
|
||||
require.Equal(t, "test", ctx.importDashboardArgs[0].PluginId)
|
||||
@ -204,10 +206,10 @@ func TestService(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, ctx.getDashboardsByPluginIdQueryArgs, 1)
|
||||
require.Equal(t, int64(2), ctx.getDashboardsByPluginIdQueryArgs[0].OrgId)
|
||||
require.Equal(t, "test", ctx.getDashboardsByPluginIdQueryArgs[0].PluginId)
|
||||
require.Empty(t, ctx.deleteDashboardArgs)
|
||||
require.Len(t, ctx.dashboardPluginService.args, 1)
|
||||
require.Equal(t, int64(2), ctx.dashboardPluginService.args[0].OrgId)
|
||||
require.Equal(t, "test", ctx.dashboardPluginService.args[0].PluginId)
|
||||
require.Empty(t, ctx.dashboardService.deleteDashboardArgs)
|
||||
})
|
||||
})
|
||||
|
||||
@ -227,21 +229,21 @@ func TestService(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginDashboards: []*plugins.PluginDashboardInfoDTO{
|
||||
pluginDashboards: []*plugindashboards.PluginDashboard{
|
||||
{
|
||||
DashboardId: 3,
|
||||
PluginId: "test",
|
||||
Path: "dashboard1.json",
|
||||
Reference: "dashboard1.json",
|
||||
},
|
||||
{
|
||||
DashboardId: 4,
|
||||
PluginId: "test",
|
||||
Path: "dashboard2.json",
|
||||
Reference: "dashboard2.json",
|
||||
},
|
||||
{
|
||||
DashboardId: 5,
|
||||
PluginId: "test",
|
||||
Path: "dashboard3.json",
|
||||
Reference: "dashboard3.json",
|
||||
},
|
||||
},
|
||||
}, func(ctx *scenarioContext) {
|
||||
@ -252,10 +254,10 @@ func TestService(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, ctx.getDashboardsByPluginIdQueryArgs, 1)
|
||||
require.Equal(t, int64(2), ctx.getDashboardsByPluginIdQueryArgs[0].OrgId)
|
||||
require.Equal(t, "test", ctx.getDashboardsByPluginIdQueryArgs[0].PluginId)
|
||||
require.Len(t, ctx.deleteDashboardArgs, 3)
|
||||
require.Len(t, ctx.dashboardPluginService.args, 1)
|
||||
require.Equal(t, int64(2), ctx.dashboardPluginService.args[0].OrgId)
|
||||
require.Equal(t, "test", ctx.dashboardPluginService.args[0].PluginId)
|
||||
require.Len(t, ctx.dashboardService.deleteDashboardArgs, 3)
|
||||
})
|
||||
|
||||
scenario(t, "When app plugin is enabled, stored disabled plugin and with dashboard updates should import dashboards",
|
||||
@ -278,25 +280,25 @@ func TestService(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginDashboards: []*plugins.PluginDashboardInfoDTO{
|
||||
pluginDashboards: []*plugindashboards.PluginDashboard{
|
||||
{
|
||||
DashboardId: 3,
|
||||
PluginId: "test",
|
||||
Path: "dashboard1.json",
|
||||
Reference: "dashboard1.json",
|
||||
Revision: 1,
|
||||
ImportedRevision: 0,
|
||||
},
|
||||
{
|
||||
DashboardId: 4,
|
||||
PluginId: "test",
|
||||
Path: "dashboard2.json",
|
||||
Reference: "dashboard2.json",
|
||||
Revision: 1,
|
||||
ImportedRevision: 0,
|
||||
},
|
||||
{
|
||||
DashboardId: 5,
|
||||
PluginId: "test",
|
||||
Path: "dashboard3.json",
|
||||
Reference: "dashboard3.json",
|
||||
Revision: 1,
|
||||
ImportedRevision: 0,
|
||||
},
|
||||
@ -309,7 +311,7 @@ func TestService(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Empty(t, ctx.deleteDashboardArgs)
|
||||
require.Empty(t, ctx.dashboardService.deleteDashboardArgs)
|
||||
|
||||
require.Len(t, ctx.importDashboardArgs, 3)
|
||||
require.Equal(t, "test", ctx.importDashboardArgs[0].PluginId)
|
||||
@ -348,23 +350,24 @@ func (m *pluginStoreMock) Plugin(ctx context.Context, pluginID string) (plugins.
|
||||
return plugins.PluginDTO{}, false
|
||||
}
|
||||
|
||||
type pluginDashboardManagerMock struct {
|
||||
plugins.PluginDashboardManager
|
||||
getPluginDashboardsFunc func(ctx context.Context, orgID int64, pluginID string) ([]*plugins.PluginDashboardInfoDTO, error)
|
||||
loadPluginDashboardFunc func(ctx context.Context, pluginID, path string) (*models.Dashboard, error)
|
||||
type pluginDashboardServiceMock struct {
|
||||
listPluginDashboardsFunc func(ctx context.Context, req *plugindashboards.ListPluginDashboardsRequest) (*plugindashboards.ListPluginDashboardsResponse, error)
|
||||
loadPluginDashboardfunc func(ctx context.Context, req *plugindashboards.LoadPluginDashboardRequest) (*plugindashboards.LoadPluginDashboardResponse, error)
|
||||
}
|
||||
|
||||
func (m *pluginDashboardManagerMock) GetPluginDashboards(ctx context.Context, orgID int64, pluginID string) ([]*plugins.PluginDashboardInfoDTO, error) {
|
||||
if m.getPluginDashboardsFunc != nil {
|
||||
return m.getPluginDashboardsFunc(ctx, orgID, pluginID)
|
||||
func (m *pluginDashboardServiceMock) ListPluginDashboards(ctx context.Context, req *plugindashboards.ListPluginDashboardsRequest) (*plugindashboards.ListPluginDashboardsResponse, error) {
|
||||
if m.listPluginDashboardsFunc != nil {
|
||||
return m.listPluginDashboardsFunc(ctx, req)
|
||||
}
|
||||
|
||||
return []*plugins.PluginDashboardInfoDTO{}, nil
|
||||
return &plugindashboards.ListPluginDashboardsResponse{
|
||||
Items: []*plugindashboards.PluginDashboard{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *pluginDashboardManagerMock) LoadPluginDashboard(ctx context.Context, pluginID, path string) (*models.Dashboard, error) {
|
||||
if m.loadPluginDashboardFunc != nil {
|
||||
return m.loadPluginDashboardFunc(ctx, pluginID, path)
|
||||
func (m *pluginDashboardServiceMock) LoadPluginDashboard(ctx context.Context, req *plugindashboards.LoadPluginDashboardRequest) (*plugindashboards.LoadPluginDashboardResponse, error) {
|
||||
if m.loadPluginDashboardfunc != nil {
|
||||
return m.loadPluginDashboardfunc(ctx, req)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
@ -418,38 +421,55 @@ func (s *pluginsSettingsServiceMock) UpdatePluginSetting(_ context.Context, _ *m
|
||||
return s.err
|
||||
}
|
||||
|
||||
type dashboardServiceMock struct {
|
||||
dashboards.DashboardService
|
||||
deleteDashboardArgs []struct {
|
||||
orgId int64
|
||||
dashboardId int64
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dashboardServiceMock) DeleteDashboard(_ context.Context, dashboardId int64, orgId int64) error {
|
||||
s.deleteDashboardArgs = append(s.deleteDashboardArgs, struct {
|
||||
orgId int64
|
||||
dashboardId int64
|
||||
}{
|
||||
orgId: orgId,
|
||||
dashboardId: dashboardId,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
type scenarioInput struct {
|
||||
storedPluginSettings []*models.PluginSettingInfoDTO
|
||||
installedPlugins []plugins.PluginDTO
|
||||
pluginDashboards []*plugins.PluginDashboardInfoDTO
|
||||
pluginDashboards []*plugindashboards.PluginDashboard
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
t *testing.T
|
||||
bus bus.Bus
|
||||
pluginSettingsService *pluginsSettingsServiceMock
|
||||
pluginStore plugins.Store
|
||||
pluginDashboardManager plugins.PluginDashboardManager
|
||||
importDashboardService dashboardimport.Service
|
||||
importDashboardArgs []*dashboardimport.ImportDashboardRequest
|
||||
deleteDashboardArgs []*models.DeleteDashboardCommand
|
||||
getPluginSettingsByIdArgs []*models.GetPluginSettingByIdQuery
|
||||
updatePluginSettingVersionArgs []*models.UpdatePluginSettingVersionCmd
|
||||
getDashboardsByPluginIdQueryArgs []*models.GetDashboardsByPluginIdQuery
|
||||
s *Service
|
||||
t *testing.T
|
||||
bus bus.Bus
|
||||
pluginSettingsService *pluginsSettingsServiceMock
|
||||
pluginStore plugins.Store
|
||||
pluginDashboardService plugindashboards.Service
|
||||
importDashboardService dashboardimport.Service
|
||||
dashboardPluginService *dashboardPluginServiceMock
|
||||
dashboardService *dashboardServiceMock
|
||||
importDashboardArgs []*dashboardimport.ImportDashboardRequest
|
||||
getPluginSettingsByIdArgs []*models.GetPluginSettingByIdQuery
|
||||
updatePluginSettingVersionArgs []*models.UpdatePluginSettingVersionCmd
|
||||
dashboardUpdater *DashboardUpdater
|
||||
}
|
||||
|
||||
func scenario(t *testing.T, desc string, input scenarioInput, f func(ctx *scenarioContext)) {
|
||||
t.Helper()
|
||||
|
||||
sCtx := &scenarioContext{
|
||||
t: t,
|
||||
bus: bus.New(),
|
||||
importDashboardArgs: []*dashboardimport.ImportDashboardRequest{},
|
||||
deleteDashboardArgs: []*models.DeleteDashboardCommand{},
|
||||
getPluginSettingsByIdArgs: []*models.GetPluginSettingByIdQuery{},
|
||||
updatePluginSettingVersionArgs: []*models.UpdatePluginSettingVersionCmd{},
|
||||
getDashboardsByPluginIdQueryArgs: []*models.GetDashboardsByPluginIdQuery{},
|
||||
t: t,
|
||||
bus: bus.New(),
|
||||
importDashboardArgs: []*dashboardimport.ImportDashboardRequest{},
|
||||
getPluginSettingsByIdArgs: []*models.GetPluginSettingByIdQuery{},
|
||||
updatePluginSettingVersionArgs: []*models.UpdatePluginSettingVersionCmd{},
|
||||
}
|
||||
|
||||
getPlugin := func(ctx context.Context, pluginID string) (plugins.PluginDTO, bool) {
|
||||
@ -470,31 +490,57 @@ func scenario(t *testing.T, desc string, input scenarioInput, f func(ctx *scenar
|
||||
pluginFunc: getPlugin,
|
||||
}
|
||||
|
||||
getPluginDashboards := func(ctx context.Context, orgID int64, pluginID string) ([]*plugins.PluginDashboardInfoDTO, error) {
|
||||
dashboards := []*plugins.PluginDashboardInfoDTO{}
|
||||
pluginDashboards := map[string][]*models.Dashboard{}
|
||||
for _, pluginDashboard := range input.pluginDashboards {
|
||||
if _, exists := pluginDashboards[pluginDashboard.PluginId]; !exists {
|
||||
pluginDashboards[pluginDashboard.PluginId] = []*models.Dashboard{}
|
||||
}
|
||||
|
||||
pluginDashboards[pluginDashboard.PluginId] = append(pluginDashboards[pluginDashboard.PluginId], &models.Dashboard{
|
||||
PluginId: pluginDashboard.PluginId,
|
||||
})
|
||||
}
|
||||
|
||||
sCtx.dashboardPluginService = &dashboardPluginServiceMock{
|
||||
pluginDashboards: pluginDashboards,
|
||||
}
|
||||
|
||||
sCtx.dashboardService = &dashboardServiceMock{
|
||||
deleteDashboardArgs: []struct {
|
||||
orgId int64
|
||||
dashboardId int64
|
||||
}{},
|
||||
}
|
||||
|
||||
listPluginDashboards := func(ctx context.Context, req *plugindashboards.ListPluginDashboardsRequest) (*plugindashboards.ListPluginDashboardsResponse, error) {
|
||||
dashboards := []*plugindashboards.PluginDashboard{}
|
||||
|
||||
for _, d := range input.pluginDashboards {
|
||||
if d.PluginId == pluginID {
|
||||
if d.PluginId == req.PluginID {
|
||||
dashboards = append(dashboards, d)
|
||||
}
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
return &plugindashboards.ListPluginDashboardsResponse{
|
||||
Items: dashboards,
|
||||
}, nil
|
||||
}
|
||||
|
||||
loadPluginDashboard := func(ctx context.Context, pluginID, path string) (*models.Dashboard, error) {
|
||||
loadPluginDashboard := func(ctx context.Context, req *plugindashboards.LoadPluginDashboardRequest) (*plugindashboards.LoadPluginDashboardResponse, error) {
|
||||
for _, d := range input.pluginDashboards {
|
||||
if d.PluginId == pluginID && path == d.Path {
|
||||
return &models.Dashboard{}, nil
|
||||
if d.PluginId == req.PluginID && req.Reference == d.Reference {
|
||||
return &plugindashboards.LoadPluginDashboardResponse{
|
||||
Dashboard: &models.Dashboard{},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no match for loading plugin dashboard")
|
||||
}
|
||||
|
||||
sCtx.pluginDashboardManager = &pluginDashboardManagerMock{
|
||||
getPluginDashboardsFunc: getPluginDashboards,
|
||||
loadPluginDashboardFunc: loadPluginDashboard,
|
||||
sCtx.pluginDashboardService = &pluginDashboardServiceMock{
|
||||
listPluginDashboardsFunc: listPluginDashboards,
|
||||
loadPluginDashboardfunc: loadPluginDashboard,
|
||||
}
|
||||
|
||||
importDashboard := func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
|
||||
@ -509,43 +555,15 @@ func scenario(t *testing.T, desc string, input scenarioInput, f func(ctx *scenar
|
||||
importDashboardFunc: importDashboard,
|
||||
}
|
||||
|
||||
sCtx.bus.AddHandler(func(ctx context.Context, cmd *models.DeleteDashboardCommand) error {
|
||||
sCtx.deleteDashboardArgs = append(sCtx.deleteDashboardArgs, cmd)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sCtx.bus.AddHandler(func(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error {
|
||||
sCtx.getDashboardsByPluginIdQueryArgs = append(sCtx.getDashboardsByPluginIdQueryArgs, query)
|
||||
dashboards := []*models.Dashboard{}
|
||||
|
||||
var plugin *models.PluginSettingInfoDTO
|
||||
|
||||
for _, p := range input.storedPluginSettings {
|
||||
if p.PluginId == query.PluginId {
|
||||
plugin = p
|
||||
}
|
||||
}
|
||||
|
||||
if plugin == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, d := range input.pluginDashboards {
|
||||
if d.PluginId == plugin.PluginId {
|
||||
dashboards = append(dashboards, &models.Dashboard{
|
||||
Id: d.DashboardId,
|
||||
OrgId: plugin.OrgId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
query.Result = dashboards
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sCtx.s = newService(sCtx.bus, sCtx.pluginStore, sCtx.pluginDashboardManager, sCtx.importDashboardService, sCtx.pluginSettingsService)
|
||||
sCtx.dashboardUpdater = newDashboardUpdater(
|
||||
sCtx.bus,
|
||||
sCtx.pluginStore,
|
||||
sCtx.pluginDashboardService,
|
||||
sCtx.importDashboardService,
|
||||
sCtx.pluginSettingsService,
|
||||
sCtx.dashboardPluginService,
|
||||
sCtx.dashboardService,
|
||||
)
|
||||
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
134
pkg/services/plugindashboards/service/service.go
Normal file
134
pkg/services/plugindashboards/service/service.go
Normal file
@ -0,0 +1,134 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
)
|
||||
|
||||
func ProvideService(pluginDashboardStore plugins.DashboardFileStore, dashboardPluginService dashboards.PluginService) *Service {
|
||||
return &Service{
|
||||
pluginDashboardStore: pluginDashboardStore,
|
||||
dashboardPluginService: dashboardPluginService,
|
||||
logger: log.New("plugindashboards"),
|
||||
}
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
pluginDashboardStore plugins.DashboardFileStore
|
||||
dashboardPluginService dashboards.PluginService
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (s Service) ListPluginDashboards(ctx context.Context, req *plugindashboards.ListPluginDashboardsRequest) (*plugindashboards.ListPluginDashboardsResponse, error) {
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("req cannot be nil")
|
||||
}
|
||||
|
||||
listArgs := &plugins.ListPluginDashboardFilesArgs{
|
||||
PluginID: req.PluginID,
|
||||
}
|
||||
listResp, err := s.pluginDashboardStore.ListPluginDashboardFiles(ctx, listArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]*plugindashboards.PluginDashboard, 0)
|
||||
|
||||
// load current dashboards
|
||||
query := models.GetDashboardsByPluginIdQuery{OrgId: req.OrgID, PluginId: req.PluginID}
|
||||
if err := s.dashboardPluginService.GetDashboardsByPluginID(ctx, &query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingMatches := make(map[int64]bool)
|
||||
for _, reference := range listResp.FileReferences {
|
||||
loadReq := &plugindashboards.LoadPluginDashboardRequest{
|
||||
PluginID: req.PluginID,
|
||||
Reference: reference,
|
||||
}
|
||||
loadResp, err := s.LoadPluginDashboard(ctx, loadReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dashboard := loadResp.Dashboard
|
||||
|
||||
res := &plugindashboards.PluginDashboard{}
|
||||
res.UID = dashboard.Uid
|
||||
res.Reference = reference
|
||||
res.PluginId = req.PluginID
|
||||
res.Title = dashboard.Title
|
||||
res.Revision = dashboard.Data.Get("revision").MustInt64(1)
|
||||
|
||||
// find existing dashboard
|
||||
for _, existingDash := range query.Result {
|
||||
if existingDash.Slug == dashboard.Slug {
|
||||
res.UID = existingDash.Uid
|
||||
res.DashboardId = existingDash.Id
|
||||
res.Imported = true
|
||||
res.ImportedUri = "db/" + existingDash.Slug
|
||||
res.ImportedUrl = existingDash.GetUrl()
|
||||
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
|
||||
existingMatches[existingDash.Id] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, res)
|
||||
}
|
||||
|
||||
// find deleted dashboards
|
||||
for _, dash := range query.Result {
|
||||
if _, exists := existingMatches[dash.Id]; !exists {
|
||||
result = append(result, &plugindashboards.PluginDashboard{
|
||||
UID: dash.Uid,
|
||||
Slug: dash.Slug,
|
||||
DashboardId: dash.Id,
|
||||
Removed: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &plugindashboards.ListPluginDashboardsResponse{
|
||||
Items: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s Service) LoadPluginDashboard(ctx context.Context, req *plugindashboards.LoadPluginDashboardRequest) (*plugindashboards.LoadPluginDashboardResponse, error) {
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("req cannot be nil")
|
||||
}
|
||||
|
||||
args := &plugins.GetPluginDashboardFileContentsArgs{
|
||||
PluginID: req.PluginID,
|
||||
FileReference: req.Reference,
|
||||
}
|
||||
resp, err := s.pluginDashboardStore.GetPluginDashboardFileContents(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := resp.Content.Close(); err != nil {
|
||||
s.logger.Warn("Failed to close plugin dashboard file", "reference", req.Reference, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := simplejson.NewFromReader(resp.Content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &plugindashboards.LoadPluginDashboardResponse{
|
||||
Dashboard: models.NewDashboardFromJson(data),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ plugindashboards.Service = &Service{}
|
224
pkg/services/plugindashboards/service/service_test.go
Normal file
224
pkg/services/plugindashboards/service/service_test.go
Normal file
@ -0,0 +1,224 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetPluginDashboards(t *testing.T) {
|
||||
testDashboardOld := simplejson.New()
|
||||
testDashboardOld.Set("title", "Nginx Connections")
|
||||
testDashboardOld.Set("revision", 22)
|
||||
|
||||
testDashboardNew := simplejson.New()
|
||||
testDashboardNew.Set("title", "Nginx Connections")
|
||||
testDashboardNew.Set("revision", 23)
|
||||
testDashboardNewBytes, err := testDashboardNew.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
testDashboardDeleted := simplejson.New()
|
||||
testDashboardDeleted.Set("title", "test")
|
||||
testDashboardDeleted.Set("id", 4)
|
||||
|
||||
pluginDashboardStore := &pluginDashboardStoreMock{
|
||||
pluginDashboardFiles: map[string]map[string][]byte{
|
||||
"test-app": {
|
||||
"nginx-connections": testDashboardNewBytes,
|
||||
},
|
||||
},
|
||||
}
|
||||
dashboardPluginService := &dashboardPluginServiceMock{
|
||||
pluginDashboards: map[string][]*models.Dashboard{
|
||||
"test-app": {
|
||||
models.NewDashboardFromJson(testDashboardOld),
|
||||
models.NewDashboardFromJson(testDashboardDeleted),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s := ProvideService(pluginDashboardStore, dashboardPluginService)
|
||||
require.NotNil(t, s)
|
||||
|
||||
t.Run("LoadPluginDashboard", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
req *plugindashboards.LoadPluginDashboardRequest
|
||||
errorFn require.ErrorAssertionFunc
|
||||
respValueFn require.ValueAssertionFunc
|
||||
validateFn func(tt *testing.T, resp *plugindashboards.LoadPluginDashboardResponse)
|
||||
}{
|
||||
{
|
||||
desc: "Should return error for nil req",
|
||||
req: nil,
|
||||
errorFn: require.Error,
|
||||
respValueFn: require.Nil,
|
||||
},
|
||||
{
|
||||
desc: "Should return error for non-existing plugin",
|
||||
req: &plugindashboards.LoadPluginDashboardRequest{
|
||||
PluginID: "non-existing",
|
||||
},
|
||||
errorFn: require.Error,
|
||||
respValueFn: require.Nil,
|
||||
},
|
||||
{
|
||||
desc: "Should return error for non-existing file reference",
|
||||
req: &plugindashboards.LoadPluginDashboardRequest{
|
||||
PluginID: "test-app",
|
||||
Reference: "non-existing",
|
||||
},
|
||||
errorFn: require.Error,
|
||||
respValueFn: require.Nil,
|
||||
},
|
||||
{
|
||||
desc: "Should return expected loaded dashboard model",
|
||||
req: &plugindashboards.LoadPluginDashboardRequest{
|
||||
PluginID: "test-app",
|
||||
Reference: "nginx-connections",
|
||||
},
|
||||
errorFn: require.NoError,
|
||||
respValueFn: require.NotNil,
|
||||
validateFn: func(tt *testing.T, resp *plugindashboards.LoadPluginDashboardResponse) {
|
||||
require.NotNil(tt, resp.Dashboard)
|
||||
require.Equal(tt, testDashboardNew.Get("title").MustString(), resp.Dashboard.Title)
|
||||
require.Equal(tt, testDashboardNew.Get("revision").MustInt64(1), resp.Dashboard.Data.Get("revision").MustInt64(1))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
resp, err := s.LoadPluginDashboard(context.Background(), tc.req)
|
||||
tc.errorFn(t, err)
|
||||
tc.respValueFn(t, resp)
|
||||
|
||||
if resp != nil {
|
||||
tc.validateFn(t, resp)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListPluginDashboards", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
req *plugindashboards.ListPluginDashboardsRequest
|
||||
errorFn require.ErrorAssertionFunc
|
||||
respValueFn require.ValueAssertionFunc
|
||||
validateFn func(tt *testing.T, resp *plugindashboards.ListPluginDashboardsResponse)
|
||||
}{
|
||||
{
|
||||
desc: "Should return error for nil req",
|
||||
req: nil,
|
||||
errorFn: require.Error,
|
||||
respValueFn: require.Nil,
|
||||
},
|
||||
{
|
||||
desc: "Should return error for non-existing plugin",
|
||||
req: &plugindashboards.ListPluginDashboardsRequest{
|
||||
PluginID: "non-existing",
|
||||
},
|
||||
errorFn: require.Error,
|
||||
respValueFn: require.Nil,
|
||||
},
|
||||
{
|
||||
desc: "Should return updated nginx dashboard revision and removed title dashboard",
|
||||
req: &plugindashboards.ListPluginDashboardsRequest{
|
||||
PluginID: "test-app",
|
||||
},
|
||||
errorFn: require.NoError,
|
||||
respValueFn: require.NotNil,
|
||||
validateFn: func(tt *testing.T, resp *plugindashboards.ListPluginDashboardsResponse) {
|
||||
require.Len(tt, resp.Items, 2)
|
||||
nginx := resp.Items[0]
|
||||
require.True(tt, nginx.Imported)
|
||||
require.Equal(t, int64(23), nginx.Revision)
|
||||
require.Equal(t, int64(22), nginx.ImportedRevision)
|
||||
require.Equal(tt, testDashboardOld.Get("title").MustString(), nginx.Title)
|
||||
test := resp.Items[1]
|
||||
require.True(tt, test.Removed)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
resp, err := s.ListPluginDashboards(context.Background(), tc.req)
|
||||
tc.errorFn(t, err)
|
||||
tc.respValueFn(t, resp)
|
||||
|
||||
if resp != nil {
|
||||
tc.validateFn(t, resp)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type pluginDashboardStoreMock struct {
|
||||
pluginDashboardFiles map[string]map[string][]byte
|
||||
}
|
||||
|
||||
func (m pluginDashboardStoreMock) ListPluginDashboardFiles(ctx context.Context, args *plugins.ListPluginDashboardFilesArgs) (*plugins.ListPluginDashboardFilesResult, error) {
|
||||
if dashboardFiles, exists := m.pluginDashboardFiles[args.PluginID]; exists {
|
||||
references := []string{}
|
||||
|
||||
for ref := range dashboardFiles {
|
||||
references = append(references, ref)
|
||||
}
|
||||
|
||||
sort.Strings(references)
|
||||
|
||||
return &plugins.ListPluginDashboardFilesResult{
|
||||
FileReferences: references,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, plugins.NotFoundError{PluginID: args.PluginID}
|
||||
}
|
||||
|
||||
func (m pluginDashboardStoreMock) GetPluginDashboardFileContents(ctx context.Context, args *plugins.GetPluginDashboardFileContentsArgs) (*plugins.GetPluginDashboardFileContentsResult, error) {
|
||||
if dashboardFiles, exists := m.pluginDashboardFiles[args.PluginID]; exists {
|
||||
if content, exists := dashboardFiles[args.FileReference]; exists {
|
||||
r := bytes.NewReader(content)
|
||||
return &plugins.GetPluginDashboardFileContentsResult{
|
||||
Content: io.NopCloser(r),
|
||||
}, nil
|
||||
}
|
||||
} else if !exists {
|
||||
return nil, plugins.NotFoundError{PluginID: args.PluginID}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("plugin dashboard file not found")
|
||||
}
|
||||
|
||||
type dashboardPluginServiceMock struct {
|
||||
pluginDashboards map[string][]*models.Dashboard
|
||||
args []*models.GetDashboardsByPluginIdQuery
|
||||
}
|
||||
|
||||
func (d *dashboardPluginServiceMock) GetDashboardsByPluginID(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error {
|
||||
query.Result = []*models.Dashboard{}
|
||||
|
||||
if dashboards, exists := d.pluginDashboards[query.PluginId]; exists {
|
||||
query.Result = dashboards
|
||||
}
|
||||
|
||||
if d.args == nil {
|
||||
d.args = []*models.GetDashboardsByPluginIdQuery{}
|
||||
}
|
||||
|
||||
d.args = append(d.args, query)
|
||||
|
||||
return nil
|
||||
}
|
@ -39,7 +39,6 @@ func (ss *SQLStore) addDashboardQueryAndCommandHandlers() {
|
||||
bus.AddHandler("sql", ss.GetDashboards)
|
||||
bus.AddHandler("sql", ss.HasEditPermissionInFolders)
|
||||
bus.AddHandler("sql", ss.GetDashboardPermissionsForUser)
|
||||
bus.AddHandler("sql", ss.GetDashboardsByPluginId)
|
||||
bus.AddHandler("sql", ss.GetDashboardSlugById)
|
||||
bus.AddHandler("sql", ss.HasAdminPermissionInFolders)
|
||||
}
|
||||
@ -441,17 +440,6 @@ func (ss *SQLStore) GetDashboardPermissionsForUser(ctx context.Context, query *m
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *SQLStore) GetDashboardsByPluginId(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error {
|
||||
return ss.WithDbSession(ctx, func(dbSession *DBSession) error {
|
||||
var dashboards = make([]*models.Dashboard, 0)
|
||||
whereExpr := "org_id=? AND plugin_id=? AND is_folder=" + dialect.BooleanStr(false)
|
||||
|
||||
err := dbSession.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards)
|
||||
query.Result = dashboards
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
type DashboardSlugDTO struct {
|
||||
Slug string
|
||||
}
|
||||
|
@ -660,10 +660,6 @@ func (m *SQLStoreMock) GetDashboardPermissionsForUser(ctx context.Context, query
|
||||
return m.ExpectedError
|
||||
}
|
||||
|
||||
func (m *SQLStoreMock) GetDashboardsByPluginId(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error {
|
||||
return m.ExpectedError
|
||||
}
|
||||
|
||||
func (m *SQLStoreMock) GetDashboardSlugById(ctx context.Context, query *models.GetDashboardSlugByIdQuery) error {
|
||||
return m.ExpectedError
|
||||
}
|
||||
|
@ -150,6 +150,5 @@ type Store interface {
|
||||
SearchOrgs(ctx context.Context, query *models.SearchOrgsQuery) error
|
||||
HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error
|
||||
GetDashboardPermissionsForUser(ctx context.Context, query *models.GetDashboardPermissionsForUserQuery) error
|
||||
GetDashboardsByPluginId(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error
|
||||
GetDashboardSlugById(ctx context.Context, query *models.GetDashboardSlugByIdQuery) error
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardimport"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
@ -61,7 +61,7 @@ func TestDashboardQuota(t *testing.T) {
|
||||
})
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
dashboardDTO := &plugins.PluginDashboardInfoDTO{}
|
||||
dashboardDTO := &plugindashboards.PluginDashboard{}
|
||||
err = json.Unmarshal(b, dashboardDTO)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 1, dashboardDTO.DashboardId)
|
||||
|
@ -49,6 +49,16 @@ func NewServer(t testing.TB, routeRegister routing.RouteRegister) *Server {
|
||||
}
|
||||
}
|
||||
|
||||
// NewGetRequest creates a new GET request setup for test.
|
||||
func (s *Server) NewGetRequest(target string) *http.Request {
|
||||
return s.NewRequest(http.MethodGet, target, nil)
|
||||
}
|
||||
|
||||
// NewPostRequest creates a new POST request setup for test.
|
||||
func (s *Server) NewPostRequest(target string, body io.Reader) *http.Request {
|
||||
return s.NewRequest(http.MethodPost, target, body)
|
||||
}
|
||||
|
||||
// NewRequest creates a new request setup for test.
|
||||
func (s *Server) NewRequest(method string, target string, body io.Reader) *http.Request {
|
||||
s.t.Helper()
|
||||
@ -65,11 +75,19 @@ func (s *Server) NewRequest(method string, target string, body io.Reader) *http.
|
||||
return req
|
||||
}
|
||||
|
||||
// Send sends an HTTP request to the test server and returns an HTTP response
|
||||
// Send sends a HTTP request to the test server and returns an HTTP response.
|
||||
func (s *Server) Send(req *http.Request) (*http.Response, error) {
|
||||
return http.DefaultClient.Do(req)
|
||||
}
|
||||
|
||||
// SendJSON sets the Content-Type header to application/json and sends
|
||||
// a HTTP request to the test server and returns an HTTP response.
|
||||
// Suitable for POST/PUT/PATCH requests that sends request body as JSON.
|
||||
func (s *Server) SendJSON(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
return s.Send(req)
|
||||
}
|
||||
|
||||
func generateRequestIdentifier() string {
|
||||
return uuid.NewString()
|
||||
}
|
||||
@ -91,7 +109,7 @@ func RequestWithWebContext(req *http.Request, c *models.ReqContext) *http.Reques
|
||||
|
||||
func RequestWithSignedInUser(req *http.Request, user *models.SignedInUser) *http.Request {
|
||||
return RequestWithWebContext(req, &models.ReqContext{
|
||||
SignedInUser: &models.SignedInUser{},
|
||||
SignedInUser: user,
|
||||
IsSignedIn: true,
|
||||
})
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
@ -12,6 +13,69 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
routeRegister := routing.NewRouteRegister()
|
||||
var actualRequest *http.Request
|
||||
routeRegister.Post("/api", routing.Wrap(func(c *models.ReqContext) response.Response {
|
||||
actualRequest = c.Req
|
||||
return response.JSON(http.StatusOK, c.SignedInUser)
|
||||
}))
|
||||
s := NewServer(t, routeRegister)
|
||||
require.NotNil(t, s)
|
||||
|
||||
t.Run("NewRequest: GET api should set expected properties", func(t *testing.T) {
|
||||
req := s.NewRequest(http.MethodGet, "api", nil)
|
||||
verifyRequest(t, s, req, "")
|
||||
})
|
||||
|
||||
t.Run("NewGetRequest: GET /api should set expected properties", func(t *testing.T) {
|
||||
req := s.NewGetRequest("/api")
|
||||
verifyRequest(t, s, req, "")
|
||||
})
|
||||
|
||||
t.Run("NewPostRequest: POST api should set expected properties", func(t *testing.T) {
|
||||
payload := strings.NewReader("test")
|
||||
req := s.NewPostRequest("api", payload)
|
||||
verifyRequest(t, s, req, "test")
|
||||
|
||||
t.Run("SendJSON should set expected Content-Type header", func(t *testing.T) {
|
||||
payload.Reset("test")
|
||||
resp, err := s.SendJSON(req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.NotNil(t, actualRequest)
|
||||
require.Equal(t, "application/json", actualRequest.Header.Get("Content-Type"))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func verifyRequest(t *testing.T, s *Server, req *http.Request, expectedBody string) {
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, s.TestServer.URL+"/api", req.URL.String())
|
||||
|
||||
if expectedBody == "" {
|
||||
require.Equal(t, http.MethodGet, req.Method)
|
||||
require.Equal(t, http.NoBody, req.Body)
|
||||
} else {
|
||||
require.Equal(t, http.MethodPost, req.Method)
|
||||
require.NotNil(t, req.Body)
|
||||
bytes, err := ioutil.ReadAll(req.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedBody, string(bytes))
|
||||
}
|
||||
|
||||
require.NotEmpty(t, requestIdentifierFromRequest(req))
|
||||
|
||||
req = RequestWithWebContext(req, &models.ReqContext{
|
||||
IsSignedIn: true,
|
||||
})
|
||||
require.NotNil(t, req)
|
||||
ctx := requestContextFromRequest(req)
|
||||
require.NotNil(t, ctx)
|
||||
require.True(t, ctx.IsSignedIn)
|
||||
}
|
||||
|
||||
func TestServerClient(t *testing.T) {
|
||||
routeRegister := routing.NewRouteRegister()
|
||||
routeRegister.Get("/test", routing.Wrap(func(c *models.ReqContext) response.Response {
|
||||
|
Loading…
Reference in New Issue
Block a user