Plugins: Make it possible to support multiple plugin versions (#82116)

* first pass

* use version in more places

* add comment

* update installer

* fix wire

* fix tests

* tidy

* simplify changes

* fix in mem

* remove unused step

* fix step dupe logic for child plugins + add tests
This commit is contained in:
Will Browne
2024-02-12 12:47:49 +01:00
committed by GitHub
parent 730e1d2485
commit 788b9afda3
24 changed files with 292 additions and 131 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ func (pm *fakePluginInstaller) Add(_ context.Context, pluginID, version string,
return nil
}
func (pm *fakePluginInstaller) Remove(_ context.Context, pluginID string) error {
func (pm *fakePluginInstaller) Remove(_ context.Context, pluginID, _ string) error {
delete(pm.plugins, pluginID)
return nil
}
+16 -7
View File
@@ -274,7 +274,12 @@ func (hs *HTTPServer) GetPluginMarkdown(c *contextmodel.ReqContext) response.Res
pluginID := web.Params(c.Req)[":pluginId"]
name := web.Params(c.Req)[":name"]
content, err := hs.pluginMarkdown(c.Req.Context(), pluginID, name)
p, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
if !exists {
return response.Error(http.StatusNotFound, "Plugin not installed", nil)
}
content, err := hs.pluginMarkdown(c.Req.Context(), pluginID, p.Info.Version, name)
if err != nil {
var notFound plugins.NotFoundError
if errors.As(err, &notFound) {
@@ -286,7 +291,7 @@ func (hs *HTTPServer) GetPluginMarkdown(c *contextmodel.ReqContext) response.Res
// fallback try readme
if len(content) == 0 {
content, err = hs.pluginMarkdown(c.Req.Context(), pluginID, "readme")
content, err = hs.pluginMarkdown(c.Req.Context(), pluginID, p.Info.Version, "readme")
if err != nil {
if errors.Is(err, plugins.ErrFileNotExist) {
return response.Error(http.StatusNotFound, plugins.ErrFileNotExist.Error(), nil)
@@ -354,7 +359,7 @@ func (hs *HTTPServer) getPluginAssets(c *contextmodel.ReqContext) {
// serveLocalPluginAsset returns the content of a plugin asset file from the local filesystem to the http client.
func (hs *HTTPServer) serveLocalPluginAsset(c *contextmodel.ReqContext, plugin pluginstore.Plugin, assetPath string) {
f, err := hs.pluginFileStore.File(c.Req.Context(), plugin.ID, assetPath)
f, err := hs.pluginFileStore.File(c.Req.Context(), plugin.ID, plugin.Info.Version, assetPath)
if err != nil {
if errors.Is(err, plugins.ErrFileNotExist) {
c.JsonApiErr(404, "Plugin file not found", nil)
@@ -476,8 +481,12 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons
func (hs *HTTPServer) UninstallPlugin(c *contextmodel.ReqContext) response.Response {
pluginID := web.Params(c.Req)[":pluginId"]
plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
if !exists {
return response.Error(http.StatusNotFound, "Plugin not installed", nil)
}
err := hs.pluginInstaller.Remove(c.Req.Context(), pluginID)
err := hs.pluginInstaller.Remove(c.Req.Context(), pluginID, plugin.Info.Version)
if err != nil {
if errors.Is(err, plugins.ErrPluginNotInstalled) {
return response.Error(http.StatusNotFound, "Plugin not installed", err)
@@ -494,19 +503,19 @@ func translatePluginRequestErrorToAPIError(err error) response.Response {
return response.ErrOrFallback(http.StatusInternalServerError, "Plugin request failed", err)
}
func (hs *HTTPServer) pluginMarkdown(ctx context.Context, pluginID string, name string) ([]byte, error) {
func (hs *HTTPServer) pluginMarkdown(ctx context.Context, pluginID, pluginVersion, name string) ([]byte, error) {
file, err := mdFilepath(strings.ToUpper(name))
if err != nil {
return make([]byte, 0), err
}
md, err := hs.pluginFileStore.File(ctx, pluginID, file)
md, err := hs.pluginFileStore.File(ctx, pluginID, pluginVersion, file)
if err != nil {
if errors.Is(err, plugins.ErrPluginNotInstalled) {
return make([]byte, 0), plugins.NotFoundError{PluginID: pluginID}
}
md, err = hs.pluginFileStore.File(ctx, pluginID, strings.ToLower(file))
md, err = hs.pluginFileStore.File(ctx, pluginID, pluginVersion, strings.ToLower(file))
if err != nil {
return make([]byte, 0), nil
}
+19 -11
View File
@@ -49,6 +49,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
canInstall := []ac.Permission{{Action: pluginaccesscontrol.ActionInstall}}
cannotInstall := []ac.Permission{{Action: "plugins:cannotinstall"}}
pluginID := "grafana-test-datasource"
localOrg := int64(1)
globalOrg := int64(ac.GlobalOrgID)
@@ -88,11 +89,17 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
hs.pluginInstaller = NewFakePluginInstaller()
hs.pluginFileStore = &fakes.FakePluginFileStore{}
hs.pluginStore = pluginstore.NewFakePluginStore(pluginstore.Plugin{
JSONData: plugins.JSONData{
ID: pluginID,
},
})
})
t.Run(testName("Install", tc), func(t *testing.T) {
input := strings.NewReader(`{"version": "1.0.2"}`)
req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/plugins/test/install", input), userWithPermissions(tc.permissionOrg, tc.permissions))
endpoint := fmt.Sprintf("/api/plugins/%s/install", pluginID)
req := webtest.RequestWithSignedInUser(server.NewPostRequest(endpoint, input), userWithPermissions(tc.permissionOrg, tc.permissions))
res, err := server.SendJSON(req)
require.NoError(t, err)
require.Equal(t, tc.expectedCode, res.StatusCode)
@@ -101,7 +108,8 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
t.Run(testName("Uninstall", tc), func(t *testing.T) {
input := strings.NewReader("{ }")
req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/plugins/test/uninstall", input), userWithPermissions(tc.permissionOrg, tc.permissions))
endpoint := fmt.Sprintf("/api/plugins/%s/uninstall", pluginID)
req := webtest.RequestWithSignedInUser(server.NewPostRequest(endpoint, input), userWithPermissions(tc.permissionOrg, tc.permissions))
res, err := server.SendJSON(req)
require.NoError(t, err)
require.Equal(t, tc.expectedCode, res.StatusCode)
@@ -401,14 +409,14 @@ func TestMakePluginResourceRequestContentTypeEmpty(t *testing.T) {
func TestPluginMarkdown(t *testing.T) {
t.Run("Plugin not installed returns error", func(t *testing.T) {
pluginFileStore := &fakes.FakePluginFileStore{
FileFunc: func(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
FileFunc: func(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error) {
return nil, plugins.ErrPluginNotInstalled
},
}
hs := HTTPServer{pluginFileStore: pluginFileStore}
pluginID := "test-datasource"
md, err := hs.pluginMarkdown(context.Background(), pluginID, "test")
md, err := hs.pluginMarkdown(context.Background(), pluginID, "", "test")
require.ErrorAs(t, err, &plugins.NotFoundError{PluginID: pluginID})
require.Equal(t, []byte{}, md)
})
@@ -416,7 +424,7 @@ func TestPluginMarkdown(t *testing.T) {
t.Run("File fetch will be retried using different casing if error occurs", func(t *testing.T) {
var requestedFiles []string
pluginFileStore := &fakes.FakePluginFileStore{
FileFunc: func(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
FileFunc: func(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error) {
requestedFiles = append(requestedFiles, filename)
return nil, errors.New("some error")
},
@@ -424,7 +432,7 @@ func TestPluginMarkdown(t *testing.T) {
hs := HTTPServer{pluginFileStore: pluginFileStore}
md, err := hs.pluginMarkdown(context.Background(), "", "reAdMe")
md, err := hs.pluginMarkdown(context.Background(), "", "", "reAdMe")
require.NoError(t, err)
require.Equal(t, []byte{}, md)
require.Equal(t, []string{"README.md", "readme.md"}, requestedFiles)
@@ -453,7 +461,7 @@ func TestPluginMarkdown(t *testing.T) {
data := []byte{123}
var requestedFiles []string
pluginFileStore := &fakes.FakePluginFileStore{
FileFunc: func(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
FileFunc: func(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error) {
requestedFiles = append(requestedFiles, filename)
return &plugins.File{Content: data}, nil
},
@@ -461,7 +469,7 @@ func TestPluginMarkdown(t *testing.T) {
hs := HTTPServer{pluginFileStore: pluginFileStore}
md, err := hs.pluginMarkdown(context.Background(), "test-datasource", tc.filePath)
md, err := hs.pluginMarkdown(context.Background(), "test-datasource", "", tc.filePath)
require.NoError(t, err)
require.Equal(t, data, md)
require.Equal(t, tc.expected, requestedFiles)
@@ -471,7 +479,7 @@ func TestPluginMarkdown(t *testing.T) {
t.Run("Non markdown file request returns an error", func(t *testing.T) {
hs := HTTPServer{pluginFileStore: &fakes.FakePluginFileStore{}}
md, err := hs.pluginMarkdown(context.Background(), "", "test.json")
md, err := hs.pluginMarkdown(context.Background(), "", "", "test.json")
require.ErrorIs(t, err, ErrUnexpectedFileExtension)
require.Equal(t, []byte{}, md)
})
@@ -480,14 +488,14 @@ func TestPluginMarkdown(t *testing.T) {
data := []byte{1, 2, 3}
pluginFileStore := &fakes.FakePluginFileStore{
FileFunc: func(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
FileFunc: func(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error) {
return &plugins.File{Content: data}, nil
},
}
hs := HTTPServer{pluginFileStore: pluginFileStore}
md, err := hs.pluginMarkdown(context.Background(), "", "someFile")
md, err := hs.pluginMarkdown(context.Background(), "", "", "someFile")
require.NoError(t, err)
require.Equal(t, data, md)
})