mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 11:42:12 +08:00
Plugins: Unexport PluginDir field from PluginDTO (#59190)
* unexport pluginDir from dto * more err checks * tidy * fix tests * fix dboard file tests * fix import * fix tests * apply PR feedback * combine interfaces * fix logs and clean up test * filepath clean * use fs.File * rm explicit type
This commit is contained in:
@ -1,12 +1,13 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -16,7 +17,6 @@ import (
|
|||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/infra/fs"
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||||
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -309,42 +310,25 @@ func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// prepend slash for cleaning relative paths
|
// prepend slash for cleaning relative paths
|
||||||
requestedFile := filepath.Clean(filepath.Join("/", web.Params(c.Req)["*"]))
|
requestedFile, err := util.CleanRelativePath(web.Params(c.Req)["*"])
|
||||||
rel, err := filepath.Rel("/", requestedFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// slash is prepended above therefore this is not expected to fail
|
// slash is prepended above therefore this is not expected to fail
|
||||||
c.JsonApiErr(500, "Failed to get the relative path", err)
|
c.JsonApiErr(500, "Failed to clean relative file path", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !plugin.IncludedInSignature(rel) {
|
f, err := plugin.File(requestedFile)
|
||||||
hs.log.Warn("Access to requested plugin file will be forbidden in upcoming Grafana versions as the file "+
|
|
||||||
"is not included in the plugin signature", "file", requestedFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
absPluginDir, err := filepath.Abs(plugin.PluginDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JsonApiErr(500, "Failed to get plugin absolute path", nil)
|
if errors.Is(err, plugins.ErrFileNotExist) {
|
||||||
return
|
c.JsonApiErr(404, "Plugin file not found", nil)
|
||||||
}
|
|
||||||
|
|
||||||
pluginFilePath := filepath.Join(absPluginDir, rel)
|
|
||||||
|
|
||||||
// It's safe to ignore gosec warning G304 since we already clean the requested file path and subsequently
|
|
||||||
// use this with a prefix of the plugin's directory, which is set during plugin loading
|
|
||||||
// nolint:gosec
|
|
||||||
f, err := os.Open(pluginFilePath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
c.JsonApiErr(404, "Plugin file not found", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JsonApiErr(500, "Could not open plugin file", err)
|
c.JsonApiErr(500, "Could not open plugin file", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := f.Close(); err != nil {
|
if err = f.Close(); err != nil {
|
||||||
hs.log.Error("Failed to close file", "err", err)
|
hs.log.Error("Failed to close plugin file", "err", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -360,7 +344,16 @@ func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) {
|
|||||||
c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
|
c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
|
||||||
}
|
}
|
||||||
|
|
||||||
http.ServeContent(c.Resp, c.Req, pluginFilePath, fi.ModTime(), f)
|
if rs, ok := f.(io.ReadSeeker); ok {
|
||||||
|
http.ServeContent(c.Resp, c.Req, requestedFile, fi.ModTime(), rs)
|
||||||
|
} else {
|
||||||
|
b, err := io.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
c.JsonApiErr(500, "Plugin file exists but could not read", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.ServeContent(c.Resp, c.Req, requestedFile, fi.ModTime(), bytes.NewReader(b))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckHealth returns the health of a plugin.
|
// CheckHealth returns the health of a plugin.
|
||||||
@ -496,34 +489,24 @@ func (hs *HTTPServer) pluginMarkdown(ctx context.Context, pluginId string, name
|
|||||||
return nil, plugins.NotFoundError{PluginID: pluginId}
|
return nil, plugins.NotFoundError{PluginID: pluginId}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:gosec
|
md, err := plugin.File(mdFilepath(strings.ToUpper(name)))
|
||||||
// We can ignore the gosec G304 warning since we have cleaned the requested file path and subsequently
|
|
||||||
// use this with a prefix of the plugin's directory, which is set during plugin loading
|
|
||||||
path := filepath.Join(plugin.PluginDir, mdFilepath(strings.ToUpper(name)))
|
|
||||||
exists, err := fs.Exists(path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
md, err = plugin.File(mdFilepath(strings.ToUpper(name)))
|
||||||
}
|
if err != nil {
|
||||||
if !exists {
|
return make([]byte, 0), nil
|
||||||
path = filepath.Join(plugin.PluginDir, mdFilepath(strings.ToLower(name)))
|
}
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = md.Close(); err != nil {
|
||||||
|
hs.log.Error("Failed to close plugin markdown file", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
exists, err = fs.Exists(path)
|
d, err := io.ReadAll(md)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return make([]byte, 0), nil
|
return make([]byte, 0), nil
|
||||||
}
|
}
|
||||||
|
return d, nil
|
||||||
// nolint:gosec
|
|
||||||
// We can ignore the gosec G304 warning since we have cleaned the requested file path and subsequently
|
|
||||||
// use this with a prefix of the plugin's directory, which is set during plugin loading
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mdFilepath(mdFilename string) string {
|
func mdFilepath(mdFilename string) string {
|
||||||
|
@ -12,7 +12,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
@ -135,13 +134,13 @@ func Test_PluginsInstallAndUninstall_AccessControl(t *testing.T) {
|
|||||||
t.Run(testName("Install", tc), func(t *testing.T) {
|
t.Run(testName("Install", tc), func(t *testing.T) {
|
||||||
input := strings.NewReader("{ \"version\": \"1.0.2\" }")
|
input := strings.NewReader("{ \"version\": \"1.0.2\" }")
|
||||||
response := callAPI(sc.server, http.MethodPost, "/api/plugins/test/install", input, t)
|
response := callAPI(sc.server, http.MethodPost, "/api/plugins/test/install", input, t)
|
||||||
assert.Equal(t, tc.expectedCode, response.Code)
|
require.Equal(t, tc.expectedCode, response.Code)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run(testName("Uninstall", tc), func(t *testing.T) {
|
t.Run(testName("Uninstall", tc), func(t *testing.T) {
|
||||||
input := strings.NewReader("{ }")
|
input := strings.NewReader("{ }")
|
||||||
response := callAPI(sc.server, http.MethodPost, "/api/plugins/test/uninstall", input, t)
|
response := callAPI(sc.server, http.MethodPost, "/api/plugins/test/uninstall", input, t)
|
||||||
assert.Equal(t, tc.expectedCode, response.Code)
|
require.Equal(t, tc.expectedCode, response.Code)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,56 +154,45 @@ func Test_GetPluginAssets(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := os.RemoveAll(tmpFile.Name())
|
err := os.RemoveAll(tmpFile.Name())
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = os.RemoveAll(tmpFileInParentDir.Name())
|
err = os.RemoveAll(tmpFileInParentDir.Name())
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
expectedBody := "Plugin test"
|
expectedBody := "Plugin test"
|
||||||
_, err = tmpFile.WriteString(expectedBody)
|
_, err = tmpFile.WriteString(expectedBody)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
requestedFile := filepath.Clean(tmpFile.Name())
|
requestedFile := filepath.Clean(tmpFile.Name())
|
||||||
|
|
||||||
t.Run("Given a request for an existing plugin file that is listed as a signature covered file", func(t *testing.T) {
|
t.Run("Given a request for an existing plugin file", func(t *testing.T) {
|
||||||
p := plugins.PluginDTO{
|
p := &plugins.Plugin{
|
||||||
JSONData: plugins.JSONData{
|
JSONData: plugins.JSONData{
|
||||||
ID: pluginID,
|
ID: pluginID,
|
||||||
},
|
},
|
||||||
PluginDir: pluginDir,
|
PluginDir: pluginDir,
|
||||||
SignedFiles: map[string]struct{}{
|
|
||||||
requestedFile: {},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
service := &plugins.FakePluginStore{
|
service := &plugins.FakePluginStore{
|
||||||
PluginList: []plugins.PluginDTO{p},
|
PluginList: []plugins.PluginDTO{p.ToDTO()},
|
||||||
}
|
}
|
||||||
l := &logtest.Fake{}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile)
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile)
|
||||||
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l,
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service,
|
||||||
func(sc *scenarioContext) {
|
func(sc *scenarioContext) {
|
||||||
callGetPluginAsset(sc)
|
callGetPluginAsset(sc)
|
||||||
|
|
||||||
require.Equal(t, 200, sc.resp.Code)
|
require.Equal(t, 200, sc.resp.Code)
|
||||||
assert.Equal(t, expectedBody, sc.resp.Body.String())
|
require.Equal(t, expectedBody, sc.resp.Body.String())
|
||||||
assert.Zero(t, l.WarnLogs.Calls)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Given a request for a relative path", func(t *testing.T) {
|
t.Run("Given a request for a relative path", func(t *testing.T) {
|
||||||
p := plugins.PluginDTO{
|
p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.External, pluginDir)
|
||||||
JSONData: plugins.JSONData{
|
|
||||||
ID: pluginID,
|
|
||||||
},
|
|
||||||
PluginDir: pluginDir,
|
|
||||||
}
|
|
||||||
service := &plugins.FakePluginStore{
|
service := &plugins.FakePluginStore{
|
||||||
PluginList: []plugins.PluginDTO{p},
|
PluginList: []plugins.PluginDTO{p},
|
||||||
}
|
}
|
||||||
l := &logtest.Fake{}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, tmpFileInParentDir.Name())
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, tmpFileInParentDir.Name())
|
||||||
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l,
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service,
|
||||||
func(sc *scenarioContext) {
|
func(sc *scenarioContext) {
|
||||||
callGetPluginAsset(sc)
|
callGetPluginAsset(sc)
|
||||||
|
|
||||||
@ -212,44 +200,15 @@ func Test_GetPluginAssets(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Given a request for an existing plugin file that is not listed as a signature covered file", func(t *testing.T) {
|
|
||||||
p := plugins.PluginDTO{
|
|
||||||
JSONData: plugins.JSONData{
|
|
||||||
ID: pluginID,
|
|
||||||
},
|
|
||||||
PluginDir: pluginDir,
|
|
||||||
}
|
|
||||||
service := &plugins.FakePluginStore{
|
|
||||||
PluginList: []plugins.PluginDTO{p},
|
|
||||||
}
|
|
||||||
l := &logtest.Fake{}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile)
|
|
||||||
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l,
|
|
||||||
func(sc *scenarioContext) {
|
|
||||||
callGetPluginAsset(sc)
|
|
||||||
|
|
||||||
require.Equal(t, 200, sc.resp.Code)
|
|
||||||
assert.Equal(t, expectedBody, sc.resp.Body.String())
|
|
||||||
assert.Zero(t, l.WarnLogs.Calls)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Given a request for an non-existing plugin file", func(t *testing.T) {
|
t.Run("Given a request for an non-existing plugin file", func(t *testing.T) {
|
||||||
p := plugins.PluginDTO{
|
p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.External, pluginDir)
|
||||||
JSONData: plugins.JSONData{
|
|
||||||
ID: pluginID,
|
|
||||||
},
|
|
||||||
PluginDir: pluginDir,
|
|
||||||
}
|
|
||||||
service := &plugins.FakePluginStore{
|
service := &plugins.FakePluginStore{
|
||||||
PluginList: []plugins.PluginDTO{p},
|
PluginList: []plugins.PluginDTO{p},
|
||||||
}
|
}
|
||||||
l := &logtest.Fake{}
|
|
||||||
|
|
||||||
requestedFile := "nonExistent"
|
requestedFile := "nonExistent"
|
||||||
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile)
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile)
|
||||||
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l,
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service,
|
||||||
func(sc *scenarioContext) {
|
func(sc *scenarioContext) {
|
||||||
callGetPluginAsset(sc)
|
callGetPluginAsset(sc)
|
||||||
|
|
||||||
@ -257,8 +216,7 @@ func Test_GetPluginAssets(t *testing.T) {
|
|||||||
err := json.NewDecoder(sc.resp.Body).Decode(&respJson)
|
err := json.NewDecoder(sc.resp.Body).Decode(&respJson)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 404, sc.resp.Code)
|
require.Equal(t, 404, sc.resp.Code)
|
||||||
assert.Equal(t, "Plugin file not found", respJson["message"])
|
require.Equal(t, "Plugin file not found", respJson["message"])
|
||||||
assert.Zero(t, l.WarnLogs.Calls)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -270,16 +228,16 @@ func Test_GetPluginAssets(t *testing.T) {
|
|||||||
|
|
||||||
requestedFile := "nonExistent"
|
requestedFile := "nonExistent"
|
||||||
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile)
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile)
|
||||||
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l,
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service,
|
||||||
func(sc *scenarioContext) {
|
func(sc *scenarioContext) {
|
||||||
callGetPluginAsset(sc)
|
callGetPluginAsset(sc)
|
||||||
|
|
||||||
var respJson map[string]interface{}
|
var respJson map[string]interface{}
|
||||||
err := json.NewDecoder(sc.resp.Body).Decode(&respJson)
|
err := json.NewDecoder(sc.resp.Body).Decode(&respJson)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 404, sc.resp.Code)
|
require.Equal(t, 404, sc.resp.Code)
|
||||||
assert.Equal(t, "Plugin not found", respJson["message"])
|
require.Equal(t, "Plugin not found", respJson["message"])
|
||||||
assert.Zero(t, l.WarnLogs.Calls)
|
require.Zero(t, l.WarnLogs.Calls)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -295,13 +253,13 @@ func Test_GetPluginAssets(t *testing.T) {
|
|||||||
l := &logtest.Fake{}
|
l := &logtest.Fake{}
|
||||||
|
|
||||||
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile)
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile)
|
||||||
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l,
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service,
|
||||||
func(sc *scenarioContext) {
|
func(sc *scenarioContext) {
|
||||||
callGetPluginAsset(sc)
|
callGetPluginAsset(sc)
|
||||||
|
|
||||||
require.Equal(t, 200, sc.resp.Code)
|
require.Equal(t, 200, sc.resp.Code)
|
||||||
assert.Equal(t, expectedBody, sc.resp.Body.String())
|
require.Equal(t, expectedBody, sc.resp.Body.String())
|
||||||
assert.Zero(t, l.WarnLogs.Calls)
|
require.Zero(t, l.WarnLogs.Calls)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -348,7 +306,7 @@ func TestMakePluginResourceRequestSetCookieNotPresent(t *testing.T) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.Empty(t, resp.Header().Values("Set-Cookie"), "Set-Cookie header should not be present")
|
require.Empty(t, resp.Header().Values("Set-Cookie"), "Set-Cookie header should not be present")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMakePluginResourceRequestContentTypeUnique(t *testing.T) {
|
func TestMakePluginResourceRequestContentTypeUnique(t *testing.T) {
|
||||||
@ -382,8 +340,8 @@ func TestMakePluginResourceRequestContentTypeUnique(t *testing.T) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.Len(t, resp.Header().Values("Content-Type"), 1, "should have 1 Content-Type header")
|
require.Len(t, resp.Header().Values("Content-Type"), 1, "should have 1 Content-Type header")
|
||||||
assert.Len(t, resp.Header().Values("x-another"), 1, "should have 1 X-Another header")
|
require.Len(t, resp.Header().Values("x-another"), 1, "should have 1 X-Another header")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -417,12 +375,11 @@ func callGetPluginAsset(sc *scenarioContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, pluginStore plugins.Store,
|
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, pluginStore plugins.Store,
|
||||||
logger log.Logger, fn scenarioFunc) {
|
fn scenarioFunc) {
|
||||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||||
hs := HTTPServer{
|
hs := HTTPServer{
|
||||||
Cfg: setting.NewCfg(),
|
Cfg: setting.NewCfg(),
|
||||||
pluginStore: pluginStore,
|
pluginStore: pluginStore,
|
||||||
log: logger,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sc := setupScenarioContext(t, url)
|
sc := setupScenarioContext(t, url)
|
||||||
@ -478,42 +435,40 @@ func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryData
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_PluginsList_AccessControl(t *testing.T) {
|
func Test_PluginsList_AccessControl(t *testing.T) {
|
||||||
pluginStore := plugins.FakePluginStore{PluginList: []plugins.PluginDTO{
|
p1 := &plugins.Plugin{
|
||||||
{
|
PluginDir: "/grafana/plugins/test-app/dist",
|
||||||
PluginDir: "/grafana/plugins/test-app/dist",
|
Class: plugins.External,
|
||||||
Class: "external",
|
DefaultNavURL: "/plugins/test-app/page/test",
|
||||||
DefaultNavURL: "/plugins/test-app/page/test",
|
Signature: plugins.SignatureUnsigned,
|
||||||
Pinned: false,
|
Module: "plugins/test-app/module",
|
||||||
Signature: "unsigned",
|
BaseURL: "public/plugins/test-app",
|
||||||
Module: "plugins/test-app/module",
|
JSONData: plugins.JSONData{
|
||||||
BaseURL: "public/plugins/test-app",
|
ID: "test-app",
|
||||||
JSONData: plugins.JSONData{
|
Type: plugins.App,
|
||||||
ID: "test-app",
|
Name: "test-app",
|
||||||
Type: "app",
|
Info: plugins.Info{
|
||||||
Name: "test-app",
|
Version: "1.0.0",
|
||||||
Info: plugins.Info{
|
|
||||||
Version: "1.0.0",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
PluginDir: "/grafana/public/app/plugins/datasource/mysql",
|
p2 := &plugins.Plugin{
|
||||||
Class: "core",
|
PluginDir: "/grafana/public/app/plugins/datasource/mysql",
|
||||||
Pinned: false,
|
Class: plugins.Core,
|
||||||
Signature: "internal",
|
Pinned: false,
|
||||||
Module: "app/plugins/datasource/mysql/module",
|
Signature: plugins.SignatureInternal,
|
||||||
BaseURL: "public/app/plugins/datasource/mysql",
|
Module: "app/plugins/datasource/mysql/module",
|
||||||
JSONData: plugins.JSONData{
|
BaseURL: "public/app/plugins/datasource/mysql",
|
||||||
ID: "mysql",
|
JSONData: plugins.JSONData{
|
||||||
Type: "datasource",
|
ID: "mysql",
|
||||||
Name: "MySQL",
|
Type: plugins.DataSource,
|
||||||
Info: plugins.Info{
|
Name: "MySQL",
|
||||||
Author: plugins.InfoLink{Name: "Grafana Labs", URL: "https://grafana.com"},
|
Info: plugins.Info{
|
||||||
Description: "Data source for MySQL databases",
|
Author: plugins.InfoLink{Name: "Grafana Labs", URL: "https://grafana.com"},
|
||||||
},
|
Description: "Data source for MySQL databases",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}
|
||||||
|
pluginStore := plugins.FakePluginStore{PluginList: []plugins.PluginDTO{p1.ToDTO(), p2.ToDTO()}}
|
||||||
|
|
||||||
pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{
|
pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{
|
||||||
"test-app": {ID: 0, OrgID: 1, PluginID: "test-app", PluginVersion: "1.0.0", Enabled: true},
|
"test-app": {ID: 0, OrgID: 1, PluginID: "test-app", PluginVersion: "1.0.0", Enabled: true},
|
||||||
@ -574,3 +529,12 @@ func Test_PluginsList_AccessControl(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createPluginDTO(jd plugins.JSONData, class plugins.Class, pluginDir string) plugins.PluginDTO {
|
||||||
|
p := &plugins.Plugin{
|
||||||
|
JSONData: jd,
|
||||||
|
Class: class,
|
||||||
|
PluginDir: pluginDir,
|
||||||
|
}
|
||||||
|
return p.ToDTO()
|
||||||
|
}
|
||||||
|
@ -2,10 +2,6 @@ package provider
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
@ -49,7 +45,7 @@ var RendererProvider PluginBackendProvider = func(_ context.Context, p *plugins.
|
|||||||
if !p.IsRenderer() {
|
if !p.IsRenderer() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return grpcplugin.NewRendererPlugin(p.ID, filepath.Join(p.PluginDir, rendererStartCmd()),
|
return grpcplugin.NewRendererPlugin(p.ID, p.ExecutablePath(),
|
||||||
func(pluginID string, renderer pluginextensionv2.RendererPlugin, logger log.Logger) error {
|
func(pluginID string, renderer pluginextensionv2.RendererPlugin, logger log.Logger) error {
|
||||||
p.Renderer = renderer
|
p.Renderer = renderer
|
||||||
return nil
|
return nil
|
||||||
@ -61,7 +57,7 @@ var SecretsManagerProvider PluginBackendProvider = func(_ context.Context, p *pl
|
|||||||
if !p.IsSecretsManager() {
|
if !p.IsSecretsManager() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return grpcplugin.NewSecretsManagerPlugin(p.ID, filepath.Join(p.PluginDir, secretsManagerStartCmd()),
|
return grpcplugin.NewSecretsManagerPlugin(p.ID, p.ExecutablePath(),
|
||||||
func(pluginID string, secretsmanager secretsmanagerplugin.SecretsManagerPlugin, logger log.Logger) error {
|
func(pluginID string, secretsmanager secretsmanagerplugin.SecretsManagerPlugin, logger log.Logger) error {
|
||||||
p.SecretsManager = secretsmanager
|
p.SecretsManager = secretsmanager
|
||||||
return nil
|
return nil
|
||||||
@ -70,42 +66,5 @@ var SecretsManagerProvider PluginBackendProvider = func(_ context.Context, p *pl
|
|||||||
}
|
}
|
||||||
|
|
||||||
var DefaultProvider PluginBackendProvider = func(_ context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc {
|
var DefaultProvider PluginBackendProvider = func(_ context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc {
|
||||||
// TODO check for executable
|
return grpcplugin.NewBackendPlugin(p.ID, p.ExecutablePath())
|
||||||
return grpcplugin.NewBackendPlugin(p.ID, filepath.Join(p.PluginDir, pluginStartCmd(p.Executable)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func pluginStartCmd(executable string) string {
|
|
||||||
os := strings.ToLower(runtime.GOOS)
|
|
||||||
arch := runtime.GOARCH
|
|
||||||
extension := ""
|
|
||||||
|
|
||||||
if os == "windows" {
|
|
||||||
extension = ".exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s_%s_%s%s", executable, os, strings.ToLower(arch), extension)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rendererStartCmd() string {
|
|
||||||
os := strings.ToLower(runtime.GOOS)
|
|
||||||
arch := runtime.GOARCH
|
|
||||||
extension := ""
|
|
||||||
|
|
||||||
if os == "windows" {
|
|
||||||
extension = ".exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s_%s_%s%s", "plugin_start", os, strings.ToLower(arch), extension)
|
|
||||||
}
|
|
||||||
|
|
||||||
func secretsManagerStartCmd() string {
|
|
||||||
os := strings.ToLower(runtime.GOOS)
|
|
||||||
arch := runtime.GOARCH
|
|
||||||
extension := ""
|
|
||||||
|
|
||||||
if os == "windows" {
|
|
||||||
extension = ".exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s_%s_%s%s", "secrets_plugin_start", os, strings.ToLower(arch), extension)
|
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
@ -24,10 +22,8 @@ func ProvideFileStoreManager(pluginStore plugins.Store) *FileStoreManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var openDashboardFile = func(name string) (fs.File, error) {
|
var openDashboardFile = func(p plugins.PluginDTO, name string) (fs.File, error) {
|
||||||
// Wrapping in filepath.Clean to properly handle
|
return p.File(name)
|
||||||
// gosec G304 Potential file inclusion via variable rule.
|
|
||||||
return os.Open(filepath.Clean(name))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FileStoreManager) ListPluginDashboardFiles(ctx context.Context, args *ListPluginDashboardFilesArgs) (*ListPluginDashboardFilesResult, error) {
|
func (m *FileStoreManager) ListPluginDashboardFiles(ctx context.Context, args *ListPluginDashboardFilesArgs) (*ListPluginDashboardFilesResult, error) {
|
||||||
@ -90,8 +86,7 @@ func (m *FileStoreManager) GetPluginDashboardFileContents(ctx context.Context, a
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboardFilePath := filepath.Join(plugin.PluginDir, cleanPath)
|
file, err := openDashboardFile(plugin, cleanPath)
|
||||||
file, err := openDashboardFile(dashboardFilePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package dashboards
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
@ -117,20 +118,24 @@ func TestDashboardFileStore(t *testing.T) {
|
|||||||
t.Run("With filesystem", func(t *testing.T) {
|
t.Run("With filesystem", func(t *testing.T) {
|
||||||
origOpenDashboardFile := openDashboardFile
|
origOpenDashboardFile := openDashboardFile
|
||||||
mapFs := fstest.MapFS{
|
mapFs := fstest.MapFS{
|
||||||
"plugins/plugin-id/dashboards/dash1.json": {
|
"dashboards/dash1.json": {
|
||||||
Data: []byte("dash1"),
|
Data: []byte("dash1"),
|
||||||
},
|
},
|
||||||
"plugins/plugin-id/dashboards/dash2.json": {
|
"dashboards/dash2.json": {
|
||||||
Data: []byte("dash2"),
|
Data: []byte("dash2"),
|
||||||
},
|
},
|
||||||
"plugins/plugin-id/dashboards/dash3.json": {
|
"dashboards/dash3.json": {
|
||||||
Data: []byte("dash3"),
|
Data: []byte("dash3"),
|
||||||
},
|
},
|
||||||
"plugins/plugin-id/dash2.json": {
|
"dash2.json": {
|
||||||
Data: []byte("dash2"),
|
Data: []byte("dash2"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
openDashboardFile = mapFs.Open
|
openDashboardFile = func(p plugins.PluginDTO, name string) (fs.File, error) {
|
||||||
|
f, err := mapFs.Open(name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
openDashboardFile = origOpenDashboardFile
|
openDashboardFile = origOpenDashboardFile
|
||||||
})
|
})
|
||||||
@ -156,7 +161,6 @@ func TestDashboardFileStore(t *testing.T) {
|
|||||||
b, err := io.ReadAll(res.Content)
|
b, err := io.ReadAll(res.Content)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "dash1", string(b))
|
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) {
|
t.Run("Should return file content for dashboards/dash2.json", func(t *testing.T) {
|
||||||
@ -170,7 +174,6 @@ func TestDashboardFileStore(t *testing.T) {
|
|||||||
b, err := io.ReadAll(res.Content)
|
b, err := io.ReadAll(res.Content)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "dash2", string(b))
|
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) {
|
t.Run("Should return error when trying to read relative file", func(t *testing.T) {
|
||||||
@ -189,39 +192,39 @@ func TestDashboardFileStore(t *testing.T) {
|
|||||||
func setupPluginDashboardsForTest(t *testing.T) *FileStoreManager {
|
func setupPluginDashboardsForTest(t *testing.T) *FileStoreManager {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return &FileStoreManager{
|
p1 := &plugins.Plugin{
|
||||||
pluginStore: &plugins.FakePluginStore{
|
JSONData: plugins.JSONData{
|
||||||
PluginList: []plugins.PluginDTO{
|
ID: "pluginWithoutDashboards",
|
||||||
|
Includes: []*plugins.Includes{
|
||||||
{
|
{
|
||||||
JSONData: plugins.JSONData{
|
Type: "page",
|
||||||
ID: "pluginWithoutDashboards",
|
|
||||||
Includes: []*plugins.Includes{
|
|
||||||
{
|
|
||||||
Type: "page",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
PluginDir: "plugins/plugin-id",
|
|
||||||
JSONData: plugins.JSONData{
|
|
||||||
ID: "pluginWithDashboards",
|
|
||||||
Includes: []*plugins.Includes{
|
|
||||||
{
|
|
||||||
Type: "page",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "dashboard",
|
|
||||||
Path: "dashboards/dash1.json",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "dashboard",
|
|
||||||
Path: "dashboards/dash2.json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p2 := &plugins.Plugin{
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: "pluginWithDashboards",
|
||||||
|
Includes: []*plugins.Includes{
|
||||||
|
{
|
||||||
|
Type: "page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "dashboard",
|
||||||
|
Path: "dashboards/dash1.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "dashboard",
|
||||||
|
Path: "dashboards/dash2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FileStoreManager{
|
||||||
|
pluginStore: &plugins.FakePluginStore{
|
||||||
|
PluginList: []plugins.PluginDTO{p1.ToDTO(), p2.ToDTO()},
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,8 +51,7 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt
|
|||||||
|
|
||||||
if plugin.Info.Version == version {
|
if plugin.Info.Version == version {
|
||||||
return plugins.DuplicateError{
|
return plugins.DuplicateError{
|
||||||
PluginID: plugin.ID,
|
PluginID: plugin.ID,
|
||||||
ExistingPluginDir: plugin.PluginDir,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,8 +64,7 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt
|
|||||||
// if existing plugin version is the same as the target update version
|
// if existing plugin version is the same as the target update version
|
||||||
if dlOpts.Version == plugin.Info.Version {
|
if dlOpts.Version == plugin.Info.Version {
|
||||||
return plugins.DuplicateError{
|
return plugins.DuplicateError{
|
||||||
PluginID: plugin.ID,
|
PluginID: plugin.ID,
|
||||||
ExistingPluginDir: plugin.PluginDir,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,8 +81,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
|
|||||||
|
|
||||||
err = inst.Add(context.Background(), pluginID, v1, plugins.CompatOpts{})
|
err = inst.Add(context.Background(), pluginID, v1, plugins.CompatOpts{})
|
||||||
require.Equal(t, plugins.DuplicateError{
|
require.Equal(t, plugins.DuplicateError{
|
||||||
PluginID: pluginV1.ID,
|
PluginID: pluginV1.ID,
|
||||||
ExistingPluginDir: pluginV1.PluginDir,
|
|
||||||
}, err)
|
}, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -127,7 +127,6 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSO
|
|||||||
plugin.Signature = sig.Status
|
plugin.Signature = sig.Status
|
||||||
plugin.SignatureType = sig.Type
|
plugin.SignatureType = sig.Type
|
||||||
plugin.SignatureOrg = sig.SigningOrg
|
plugin.SignatureOrg = sig.SigningOrg
|
||||||
plugin.SignedFiles = sig.Files
|
|
||||||
|
|
||||||
loadedPlugins[plugin.PluginDir] = plugin
|
loadedPlugins[plugin.PluginDir] = plugin
|
||||||
}
|
}
|
||||||
|
@ -116,8 +116,8 @@ func TestIntegrationPluginManager(t *testing.T) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
verifyCorePluginCatalogue(t, ctx, ps)
|
verifyCorePluginCatalogue(t, ctx, ps)
|
||||||
verifyBundledPlugins(t, ctx, ps)
|
verifyBundledPlugins(t, ctx, ps, reg)
|
||||||
verifyPluginStaticRoutes(t, ctx, ps)
|
verifyPluginStaticRoutes(t, ctx, ps, reg)
|
||||||
verifyBackendProcesses(t, reg.Plugins(ctx))
|
verifyBackendProcesses(t, reg.Plugins(ctx))
|
||||||
verifyPluginQuery(t, ctx, client.ProvideService(reg, pCfg))
|
verifyPluginQuery(t, ctx, client.ProvideService(reg, pCfg))
|
||||||
}
|
}
|
||||||
@ -245,7 +245,7 @@ func verifyCorePluginCatalogue(t *testing.T, ctx context.Context, ps *store.Serv
|
|||||||
require.Equal(t, len(expPanels)+len(expDataSources)+len(expApps), len(ps.Plugins(ctx)))
|
require.Equal(t, len(expPanels)+len(expDataSources)+len(expApps), len(ps.Plugins(ctx)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyBundledPlugins(t *testing.T, ctx context.Context, ps *store.Service) {
|
func verifyBundledPlugins(t *testing.T, ctx context.Context, ps *store.Service, reg registry.Service) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
dsPlugins := make(map[string]struct{})
|
dsPlugins := make(map[string]struct{})
|
||||||
@ -258,6 +258,9 @@ func verifyBundledPlugins(t *testing.T, ctx context.Context, ps *store.Service)
|
|||||||
require.NotEqual(t, plugins.PluginDTO{}, inputPlugin)
|
require.NotEqual(t, plugins.PluginDTO{}, inputPlugin)
|
||||||
require.NotNil(t, dsPlugins["input"])
|
require.NotNil(t, dsPlugins["input"])
|
||||||
|
|
||||||
|
intInputPlugin, exists := reg.Plugin(ctx, "input")
|
||||||
|
require.True(t, exists)
|
||||||
|
|
||||||
pluginRoutes := make(map[string]*plugins.StaticRoute)
|
pluginRoutes := make(map[string]*plugins.StaticRoute)
|
||||||
for _, r := range ps.Routes() {
|
for _, r := range ps.Routes() {
|
||||||
pluginRoutes[r.PluginID] = r
|
pluginRoutes[r.PluginID] = r
|
||||||
@ -265,23 +268,23 @@ func verifyBundledPlugins(t *testing.T, ctx context.Context, ps *store.Service)
|
|||||||
|
|
||||||
for _, pluginID := range []string{"input"} {
|
for _, pluginID := range []string{"input"} {
|
||||||
require.Contains(t, pluginRoutes, pluginID)
|
require.Contains(t, pluginRoutes, pluginID)
|
||||||
require.True(t, strings.HasPrefix(pluginRoutes[pluginID].Directory, inputPlugin.PluginDir))
|
require.True(t, strings.HasPrefix(pluginRoutes[pluginID].Directory, intInputPlugin.PluginDir))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyPluginStaticRoutes(t *testing.T, ctx context.Context, ps *store.Service) {
|
func verifyPluginStaticRoutes(t *testing.T, ctx context.Context, rr plugins.StaticRouteResolver, reg registry.Service) {
|
||||||
routes := make(map[string]*plugins.StaticRoute)
|
routes := make(map[string]*plugins.StaticRoute)
|
||||||
for _, route := range ps.Routes() {
|
for _, route := range rr.Routes() {
|
||||||
routes[route.PluginID] = route
|
routes[route.PluginID] = route
|
||||||
}
|
}
|
||||||
|
|
||||||
require.Len(t, routes, 2)
|
require.Len(t, routes, 2)
|
||||||
|
|
||||||
inputPlugin, _ := ps.Plugin(ctx, "input")
|
inputPlugin, _ := reg.Plugin(ctx, "input")
|
||||||
require.NotNil(t, routes["input"])
|
require.NotNil(t, routes["input"])
|
||||||
require.Equal(t, routes["input"].Directory, inputPlugin.PluginDir)
|
require.Equal(t, routes["input"].Directory, inputPlugin.PluginDir)
|
||||||
|
|
||||||
testAppPlugin, _ := ps.Plugin(ctx, "test-app")
|
testAppPlugin, _ := reg.Plugin(ctx, "test-app")
|
||||||
require.Contains(t, routes, "test-app")
|
require.Contains(t, routes, "test-app")
|
||||||
require.Equal(t, routes["test-app"].Directory, testAppPlugin.PluginDir)
|
require.Equal(t, routes["test-app"].Directory, testAppPlugin.PluginDir)
|
||||||
}
|
}
|
||||||
|
@ -111,12 +111,7 @@ func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, erro
|
|||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
manifestPath := filepath.Join(plugin.PluginDir, "MANIFEST.txt")
|
byteValue := plugin.Manifest()
|
||||||
|
|
||||||
// nolint:gosec
|
|
||||||
// We can ignore the gosec G304 warning on this one because `manifestPath` is based
|
|
||||||
// on plugin the folder structure on disk and not user input.
|
|
||||||
byteValue, err := os.ReadFile(manifestPath)
|
|
||||||
if err != nil || len(byteValue) < 10 {
|
if err != nil || len(byteValue) < 10 {
|
||||||
mlog.Debug("Plugin is unsigned", "id", plugin.ID)
|
mlog.Debug("Plugin is unsigned", "id", plugin.ID)
|
||||||
return plugins.Signature{
|
return plugins.Signature{
|
||||||
|
@ -54,7 +54,7 @@ func (s *Validator) Validate(plugin *plugins.Plugin) *plugins.SignatureError {
|
|||||||
SignatureStatus: plugins.SignatureUnsigned,
|
SignatureStatus: plugins.SignatureUnsigned,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.log.Warn("Permitting unsigned plugin. This is not recommended", "pluginID", plugin.ID, "pluginDir", plugin.PluginDir)
|
s.log.Warn("Permitting unsigned plugin. This is not recommended", "pluginID", plugin.ID)
|
||||||
return nil
|
return nil
|
||||||
case plugins.SignatureInvalid:
|
case plugins.SignatureInvalid:
|
||||||
s.log.Debug("Plugin has an invalid signature", "pluginID", plugin.ID)
|
s.log.Debug("Plugin has an invalid signature", "pluginID", plugin.ID)
|
||||||
|
@ -26,12 +26,11 @@ func (e NotFoundError) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DuplicateError struct {
|
type DuplicateError struct {
|
||||||
PluginID string
|
PluginID string
|
||||||
ExistingPluginDir string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e DuplicateError) Error() string {
|
func (e DuplicateError) Error() string {
|
||||||
return fmt.Sprintf("plugin with ID '%s' already exists in '%s'", e.PluginID, e.ExistingPluginDir)
|
return fmt.Sprintf("plugin with ID '%s' already exists", e.PluginID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e DuplicateError) Is(err error) bool {
|
func (e DuplicateError) Is(err error) bool {
|
||||||
@ -195,13 +194,10 @@ func (s SignatureType) IsValid() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginFiles map[string]struct{}
|
|
||||||
|
|
||||||
type Signature struct {
|
type Signature struct {
|
||||||
Status SignatureStatus
|
Status SignatureStatus
|
||||||
Type SignatureType
|
Type SignatureType
|
||||||
SigningOrg string
|
SigningOrg string
|
||||||
Files PluginFiles
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginMetaDTO struct {
|
type PluginMetaDTO struct {
|
||||||
|
@ -4,6 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
@ -11,8 +16,11 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
|
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrFileNotExist = fmt.Errorf("file does not exist")
|
||||||
|
|
||||||
type Plugin struct {
|
type Plugin struct {
|
||||||
JSONData
|
JSONData
|
||||||
|
|
||||||
@ -30,7 +38,6 @@ type Plugin struct {
|
|||||||
SignatureOrg string
|
SignatureOrg string
|
||||||
Parent *Plugin
|
Parent *Plugin
|
||||||
Children []*Plugin
|
Children []*Plugin
|
||||||
SignedFiles PluginFiles
|
|
||||||
SignatureError *SignatureError
|
SignatureError *SignatureError
|
||||||
|
|
||||||
// SystemJS fields
|
// SystemJS fields
|
||||||
@ -46,8 +53,10 @@ type Plugin struct {
|
|||||||
type PluginDTO struct {
|
type PluginDTO struct {
|
||||||
JSONData
|
JSONData
|
||||||
|
|
||||||
PluginDir string
|
logger log.Logger
|
||||||
Class Class
|
pluginDir string
|
||||||
|
|
||||||
|
Class Class
|
||||||
|
|
||||||
// App fields
|
// App fields
|
||||||
IncludedInAppID string
|
IncludedInAppID string
|
||||||
@ -58,7 +67,6 @@ type PluginDTO struct {
|
|||||||
Signature SignatureStatus
|
Signature SignatureStatus
|
||||||
SignatureType SignatureType
|
SignatureType SignatureType
|
||||||
SignatureOrg string
|
SignatureOrg string
|
||||||
SignedFiles PluginFiles
|
|
||||||
SignatureError *SignatureError
|
SignatureError *SignatureError
|
||||||
|
|
||||||
// SystemJS fields
|
// SystemJS fields
|
||||||
@ -89,21 +97,29 @@ func (p PluginDTO) IsSecretsManager() bool {
|
|||||||
return p.JSONData.Type == SecretsManager
|
return p.JSONData.Type == SecretsManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PluginDTO) IncludedInSignature(file string) bool {
|
func (p PluginDTO) File(name string) (fs.File, error) {
|
||||||
// permit Core plugin files
|
cleanPath, err := util.CleanRelativePath(name)
|
||||||
if p.IsCorePlugin() {
|
if err != nil {
|
||||||
return true
|
// CleanRelativePath should clean and make the path relative so this is not expected to fail
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// permit when no signed files (no MANIFEST)
|
absPluginDir, err := filepath.Abs(p.pluginDir)
|
||||||
if p.SignedFiles == nil {
|
if err != nil {
|
||||||
return true
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, exists := p.SignedFiles[file]; !exists {
|
absFilePath := filepath.Join(absPluginDir, cleanPath)
|
||||||
return false
|
// Wrapping in filepath.Clean to properly handle
|
||||||
|
// gosec G304 Potential file inclusion via variable rule.
|
||||||
|
f, err := os.Open(filepath.Clean(absFilePath))
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, ErrFileNotExist
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return true
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONData represents the plugin's plugin.json
|
// JSONData represents the plugin's plugin.json
|
||||||
@ -318,6 +334,25 @@ func (p *Plugin) Client() (PluginClient, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) ExecutablePath() string {
|
||||||
|
os := strings.ToLower(runtime.GOOS)
|
||||||
|
arch := runtime.GOARCH
|
||||||
|
extension := ""
|
||||||
|
|
||||||
|
if os == "windows" {
|
||||||
|
extension = ".exe"
|
||||||
|
}
|
||||||
|
if p.IsRenderer() {
|
||||||
|
return filepath.Join(p.PluginDir, fmt.Sprintf("%s_%s_%s%s", "plugin_start", os, strings.ToLower(arch), extension))
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.IsSecretsManager() {
|
||||||
|
return filepath.Join(p.PluginDir, fmt.Sprintf("%s_%s_%s%s", "secrets_plugin_start", os, strings.ToLower(arch), extension))
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(p.PluginDir, fmt.Sprintf("%s_%s_%s%s", p.Executable, os, strings.ToLower(arch), extension))
|
||||||
|
}
|
||||||
|
|
||||||
type PluginClient interface {
|
type PluginClient interface {
|
||||||
backend.QueryDataHandler
|
backend.QueryDataHandler
|
||||||
backend.CollectMetricsHandler
|
backend.CollectMetricsHandler
|
||||||
@ -330,8 +365,9 @@ func (p *Plugin) ToDTO() PluginDTO {
|
|||||||
c, _ := p.Client()
|
c, _ := p.Client()
|
||||||
|
|
||||||
return PluginDTO{
|
return PluginDTO{
|
||||||
|
logger: p.Logger(),
|
||||||
|
pluginDir: p.PluginDir,
|
||||||
JSONData: p.JSONData,
|
JSONData: p.JSONData,
|
||||||
PluginDir: p.PluginDir,
|
|
||||||
Class: p.Class,
|
Class: p.Class,
|
||||||
IncludedInAppID: p.IncludedInAppID,
|
IncludedInAppID: p.IncludedInAppID,
|
||||||
DefaultNavURL: p.DefaultNavURL,
|
DefaultNavURL: p.DefaultNavURL,
|
||||||
@ -339,7 +375,6 @@ func (p *Plugin) ToDTO() PluginDTO {
|
|||||||
Signature: p.Signature,
|
Signature: p.Signature,
|
||||||
SignatureType: p.SignatureType,
|
SignatureType: p.SignatureType,
|
||||||
SignatureOrg: p.SignatureOrg,
|
SignatureOrg: p.SignatureOrg,
|
||||||
SignedFiles: p.SignedFiles,
|
|
||||||
SignatureError: p.SignatureError,
|
SignatureError: p.SignatureError,
|
||||||
Module: p.Module,
|
Module: p.Module,
|
||||||
BaseURL: p.BaseURL,
|
BaseURL: p.BaseURL,
|
||||||
@ -387,6 +422,15 @@ func (p *Plugin) IsExternalPlugin() bool {
|
|||||||
return p.Class == External
|
return p.Class == External
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) Manifest() []byte {
|
||||||
|
d, err := os.ReadFile(filepath.Join(p.PluginDir, "MANIFEST.txt"))
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
type Class string
|
type Class string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -116,7 +116,7 @@ func (s Service) LoadPluginDashboard(ctx context.Context, req *plugindashboards.
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := resp.Content.Close(); err != nil {
|
if err = resp.Content.Close(); err != nil {
|
||||||
s.logger.Warn("Failed to close plugin dashboard file", "reference", req.Reference, "err", err)
|
s.logger.Warn("Failed to close plugin dashboard file", "reference", req.Reference, "err", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -191,9 +191,8 @@ func (m pluginDashboardStoreMock) ListPluginDashboardFiles(ctx context.Context,
|
|||||||
func (m pluginDashboardStoreMock) GetPluginDashboardFileContents(ctx context.Context, args *dashboards.GetPluginDashboardFileContentsArgs) (*dashboards.GetPluginDashboardFileContentsResult, error) {
|
func (m pluginDashboardStoreMock) GetPluginDashboardFileContents(ctx context.Context, args *dashboards.GetPluginDashboardFileContentsArgs) (*dashboards.GetPluginDashboardFileContentsResult, error) {
|
||||||
if dashboardFiles, exists := m.pluginDashboardFiles[args.PluginID]; exists {
|
if dashboardFiles, exists := m.pluginDashboardFiles[args.PluginID]; exists {
|
||||||
if content, exists := dashboardFiles[args.FileReference]; exists {
|
if content, exists := dashboardFiles[args.FileReference]; exists {
|
||||||
r := bytes.NewReader(content)
|
|
||||||
return &dashboards.GetPluginDashboardFileContentsResult{
|
return &dashboards.GetPluginDashboardFileContentsResult{
|
||||||
Content: io.NopCloser(r),
|
Content: io.NopCloser(bytes.NewReader(content)),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
} else if !exists {
|
} else if !exists {
|
||||||
|
Reference in New Issue
Block a user