grafana/pkg/plugins/manager/manager_test.go
Will Browne c39d6ad97d
Plugins: Enable plugin runtime install/uninstall capabilities (#33836)
* add uninstall flow

* add install flow

* small cleanup

* smaller-footprint solution

* cleanup + make bp start auto

* fix interface contract

* improve naming

* accept version arg

* ensure use of shared logger

* make installer a field

* add plugin decommissioning

* add basic error checking

* fix api docs

* making initialization idempotent

* add mutex

* fix comment

* fix test

* add test for decommission

* improve existing test

* add more test coverage

* more tests

* change test func to use read lock

* refactoring + adding test asserts

* improve purging old install flow

* improve dupe checking

* change log name

* skip over dupe scanned

* make test assertion more flexible

* remove trailing line

* fix pointer receiver name

* update comment

* add context to API

* add config flag

* add base http api test + fix update functionality

* simplify existing check

* clean up test

* refactor tests based on feedback

* add single quotes to errs

* use gcmp in tests + fix logo issue

* make plugin list testing more flexible

* address feedback

* fix API test

* fix linter

* undo preallocate

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* fix linting issue in test

* add docs placeholder

* update install notes

* Update docs/sources/plugins/marketplace.md

Co-authored-by: Marcus Olsson <marcus.olsson@hey.com>

* update access wording

* add more placeholder docs

* add link to more info

* PR feedback - improved errors, refactor, lock fix

* improve err details

* propagate plugin version errors

* don't autostart renderer

* add H1

* fix imports

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
Co-authored-by: Marcus Olsson <marcus.olsson@hey.com>
2021-05-12 20:05:16 +02:00

567 lines
16 KiB
Go

package manager
import (
"context"
"errors"
"fmt"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
)
func TestPluginManager_Init(t *testing.T) {
t.Run("Base case (core + bundled plugins)", func(t *testing.T) {
staticRootPath, err := filepath.Abs("../../../public")
require.NoError(t, err)
bundledPluginsPath, err := filepath.Abs("../../../plugins-bundled/internal")
require.NoError(t, err)
pm := createManager(t, func(pm *PluginManager) {
pm.Cfg.PluginsPath = ""
pm.Cfg.BundledPluginsPath = bundledPluginsPath
pm.Cfg.StaticRootPath = staticRootPath
})
err = pm.Init()
require.NoError(t, err)
assert.Empty(t, pm.scanningErrors)
verifyCorePluginCatalogue(t, pm)
// verify bundled plugins
assert.NotNil(t, pm.plugins["input"])
assert.NotNil(t, pm.dataSources["input"])
assert.Len(t, pm.StaticRoutes(), 1)
assert.Equal(t, "input", pm.StaticRoutes()[0].PluginId)
assert.True(t, strings.HasPrefix(pm.StaticRoutes()[0].Directory, bundledPluginsPath+"/input-datasource/"))
})
t.Run("Base case with single external plugin", func(t *testing.T) {
pm := createManager(t, func(pm *PluginManager) {
pm.Cfg.PluginSettings = setting.PluginSettings{
"nginx-app": map[string]string{
"path": "testdata/test-app",
},
}
})
err := pm.Init()
require.NoError(t, err)
assert.Empty(t, pm.scanningErrors)
verifyCorePluginCatalogue(t, pm)
assert.NotEmpty(t, pm.apps)
assert.Equal(t, "app/plugins/datasource/graphite/module", pm.dataSources["graphite"].Module)
assert.Equal(t, "public/plugins/test-app/img/logo_large.png", pm.apps["test-app"].Info.Logos.Large)
assert.Equal(t, "public/plugins/test-app/img/screenshot2.png", pm.apps["test-app"].Info.Screenshots[1].Path)
})
t.Run("With external back-end plugin lacking signature", func(t *testing.T) {
pm := createManager(t, func(pm *PluginManager) {
pm.Cfg.PluginsPath = "testdata/unsigned"
})
err := pm.Init()
require.NoError(t, err)
})
t.Run("With external unsigned back-end plugin and configuration disabling signature check of this plugin", func(t *testing.T) {
pm := createManager(t, func(pm *PluginManager) {
pm.Cfg.PluginsPath = "testdata/unsigned"
pm.Cfg.PluginsAllowUnsigned = []string{"test"}
})
err := pm.Init()
require.NoError(t, err)
assert.Empty(t, pm.scanningErrors)
})
t.Run("With external back-end plugin with invalid v1 signature", func(t *testing.T) {
pm := createManager(t, func(pm *PluginManager) {
pm.Cfg.PluginsPath = "testdata/invalid-v1-signature"
})
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test" has an invalid signature`)}, pm.scanningErrors)
})
t.Run("With external back-end plugin lacking files listed in manifest", func(t *testing.T) {
fm := &fakeBackendPluginManager{}
pm := createManager(t, func(pm *PluginManager) {
pm.Cfg.PluginsPath = "testdata/lacking-files"
pm.BackendPluginManager = fm
})
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
})
t.Run("Transform plugins should be ignored when expressions feature is off", func(t *testing.T) {
fm := fakeBackendPluginManager{}
pm := createManager(t, func(pm *PluginManager) {
pm.Cfg.PluginsPath = "testdata/behind-feature-flag"
pm.BackendPluginManager = &fm
})
err := pm.Init()
require.NoError(t, err)
assert.Empty(t, pm.scanningErrors)
assert.Empty(t, fm.registeredPlugins)
})
t.Run("With nested plugin duplicating parent", func(t *testing.T) {
pm := createManager(t, func(pm *PluginManager) {
pm.Cfg.PluginsPath = "testdata/duplicate-plugins"
})
err := pm.Init()
require.NoError(t, err)
assert.Len(t, pm.scanningErrors, 1)
assert.True(t, errors.Is(pm.scanningErrors[0], plugins.DuplicatePluginError{}))
})
t.Run("With external back-end plugin with valid v2 signature", func(t *testing.T) {
const pluginsDir = "testdata/valid-v2-signature"
const pluginFolder = pluginsDir + "/plugin"
pm := createManager(t, func(manager *PluginManager) {
manager.Cfg.PluginsPath = pluginsDir
})
err := pm.Init()
require.NoError(t, err)
require.Empty(t, pm.scanningErrors)
// capture manager plugin state
datasources := pm.dataSources
panels := pm.panels
apps := pm.apps
verifyPluginManagerState := func() {
assert.Empty(t, pm.scanningErrors)
verifyCorePluginCatalogue(t, pm)
// verify plugin has been loaded successfully
const pluginID = "test"
if diff := cmp.Diff(&plugins.PluginBase{
Type: "datasource",
Name: "Test",
State: "alpha",
Id: pluginID,
Info: plugins.PluginInfo{
Author: plugins.PluginInfoLink{
Name: "Will Browne",
Url: "https://willbrowne.com",
},
Description: "Test",
Logos: plugins.PluginLogos{
Small: "public/img/icn-datasource.svg",
Large: "public/img/icn-datasource.svg",
},
Build: plugins.PluginBuildInfo{},
Version: "1.0.0",
},
PluginDir: pluginFolder,
Backend: false,
IsCorePlugin: false,
Signature: plugins.PluginSignatureValid,
SignatureType: plugins.GrafanaType,
SignatureOrg: "Grafana Labs",
Dependencies: plugins.PluginDependencies{
GrafanaVersion: "*",
Plugins: []plugins.PluginDependencyItem{},
},
Module: "plugins/test/module",
BaseUrl: "public/plugins/test",
}, pm.plugins[pluginID]); diff != "" {
t.Errorf("result mismatch (-want +got) %s\n", diff)
}
ds := pm.GetDataSource(pluginID)
assert.NotNil(t, ds)
assert.Equal(t, pluginID, ds.Id)
assert.Equal(t, pm.plugins[pluginID], &ds.FrontendPluginBase.PluginBase)
assert.Len(t, pm.StaticRoutes(), 1)
assert.Equal(t, pluginID, pm.StaticRoutes()[0].PluginId)
assert.Equal(t, pluginFolder, pm.StaticRoutes()[0].Directory)
}
verifyPluginManagerState()
t.Run("Re-initializing external plugins is idempotent", func(t *testing.T) {
err = pm.initExternalPlugins()
require.NoError(t, err)
// verify plugin state remains the same as previous
verifyPluginManagerState()
assert.Empty(t, pm.scanningErrors)
assert.True(t, reflect.DeepEqual(datasources, pm.dataSources))
assert.True(t, reflect.DeepEqual(panels, pm.panels))
assert.True(t, reflect.DeepEqual(apps, pm.apps))
})
})
t.Run("With back-end plugin with invalid v2 private signature (mismatched root URL)", func(t *testing.T) {
origAppURL := setting.AppUrl
t.Cleanup(func() {
setting.AppUrl = origAppURL
})
setting.AppUrl = "http://localhost:1234"
pm := createManager(t, func(pm *PluginManager) {
pm.Cfg.PluginsPath = "testdata/valid-v2-pvt-signature"
})
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test" has an invalid signature`)}, pm.scanningErrors)
assert.Nil(t, pm.plugins[("test")])
})
t.Run("With back-end plugin with valid v2 private signature", func(t *testing.T) {
origAppURL := setting.AppUrl
t.Cleanup(func() {
setting.AppUrl = origAppURL
})
setting.AppUrl = "http://localhost:3000/"
pm := createManager(t, func(pm *PluginManager) {
pm.Cfg.PluginsPath = "testdata/valid-v2-pvt-signature"
})
err := pm.Init()
require.NoError(t, err)
require.Empty(t, pm.scanningErrors)
const pluginID = "test"
assert.NotNil(t, pm.plugins[pluginID])
assert.Equal(t, "datasource", pm.plugins[pluginID].Type)
assert.Equal(t, "Test", pm.plugins[pluginID].Name)
assert.Equal(t, pluginID, pm.plugins[pluginID].Id)
assert.Equal(t, "1.0.0", pm.plugins[pluginID].Info.Version)
assert.Equal(t, plugins.PluginSignatureValid, pm.plugins[pluginID].Signature)
assert.Equal(t, plugins.PrivateType, pm.plugins[pluginID].SignatureType)
assert.Equal(t, "Will Browne", pm.plugins[pluginID].SignatureOrg)
assert.False(t, pm.plugins[pluginID].IsCorePlugin)
})
t.Run("With back-end plugin with modified v2 signature (missing file from plugin dir)", func(t *testing.T) {
origAppURL := setting.AppUrl
t.Cleanup(func() {
setting.AppUrl = origAppURL
})
setting.AppUrl = "http://localhost:3000/"
pm := createManager(t, func(pm *PluginManager) {
pm.Cfg.PluginsPath = "testdata/invalid-v2-signature"
})
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
assert.Nil(t, pm.plugins[("test")])
})
t.Run("With back-end plugin with modified v2 signature (unaccounted file in plugin dir)", func(t *testing.T) {
origAppURL := setting.AppUrl
t.Cleanup(func() {
setting.AppUrl = origAppURL
})
setting.AppUrl = "http://localhost:3000/"
pm := createManager(t, func(pm *PluginManager) {
pm.Cfg.PluginsPath = "testdata/invalid-v2-signature-2"
})
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
assert.Nil(t, pm.plugins[("test")])
})
}
func TestPluginManager_IsBackendOnlyPlugin(t *testing.T) {
pluginScanner := &PluginScanner{}
type testCase struct {
name string
isBackendOnly bool
}
for _, c := range []testCase{
{name: "renderer", isBackendOnly: true},
{name: "app", isBackendOnly: false},
} {
t.Run(fmt.Sprintf("Plugin %s", c.name), func(t *testing.T) {
result := pluginScanner.IsBackendOnlyPlugin(c.name)
assert.Equal(t, c.isBackendOnly, result)
})
}
}
func TestPluginManager_Installer(t *testing.T) {
t.Run("Install plugin after manager init", func(t *testing.T) {
fm := &fakeBackendPluginManager{}
pm := createManager(t, func(pm *PluginManager) {
pm.BackendPluginManager = fm
})
err := pm.Init()
require.NoError(t, err)
// mock installer
installer := &fakePluginInstaller{}
pm.pluginInstaller = installer
// Set plugin location (we do this after manager Init() so that
// it doesn't install the plugin automatically)
pm.Cfg.PluginsPath = "testdata/installer"
pluginID := "test"
pluginFolder := pm.Cfg.PluginsPath + "/plugin"
err = pm.Install(context.Background(), pluginID, "1.0.0")
require.NoError(t, err)
assert.Equal(t, 1, installer.installCount)
assert.Equal(t, 0, installer.uninstallCount)
// verify plugin manager has loaded core plugins successfully
assert.Empty(t, pm.scanningErrors)
verifyCorePluginCatalogue(t, pm)
// verify plugin has been loaded successfully
assert.NotNil(t, pm.plugins[pluginID])
if diff := cmp.Diff(&plugins.PluginBase{
Type: "datasource",
Name: "Test",
State: "alpha",
Id: pluginID,
Info: plugins.PluginInfo{
Author: plugins.PluginInfoLink{
Name: "Will Browne",
Url: "https://willbrowne.com",
},
Description: "Test",
Logos: plugins.PluginLogos{
Small: "public/img/icn-datasource.svg",
Large: "public/img/icn-datasource.svg",
},
Build: plugins.PluginBuildInfo{},
Version: "1.0.0",
},
PluginDir: pluginFolder,
Backend: false,
IsCorePlugin: false,
Signature: plugins.PluginSignatureValid,
SignatureType: plugins.GrafanaType,
SignatureOrg: "Grafana Labs",
Dependencies: plugins.PluginDependencies{
GrafanaVersion: "*",
Plugins: []plugins.PluginDependencyItem{},
},
Module: "plugins/test/module",
BaseUrl: "public/plugins/test",
}, pm.plugins[pluginID]); diff != "" {
t.Errorf("result mismatch (-want +got) %s\n", diff)
}
ds := pm.GetDataSource(pluginID)
assert.NotNil(t, ds)
assert.Equal(t, pluginID, ds.Id)
assert.Equal(t, pm.plugins[pluginID], &ds.FrontendPluginBase.PluginBase)
assert.Len(t, pm.StaticRoutes(), 1)
assert.Equal(t, pluginID, pm.StaticRoutes()[0].PluginId)
assert.Equal(t, pluginFolder, pm.StaticRoutes()[0].Directory)
t.Run("Won't install if already installed", func(t *testing.T) {
err := pm.Install(context.Background(), pluginID, "1.0.0")
require.Equal(t, plugins.DuplicatePluginError{
PluginID: pluginID,
ExistingPluginDir: pluginFolder,
}, err)
})
t.Run("Uninstall base case", func(t *testing.T) {
err := pm.Uninstall(context.Background(), pluginID)
require.NoError(t, err)
assert.Equal(t, 1, installer.installCount)
assert.Equal(t, 1, installer.uninstallCount)
assert.Nil(t, pm.GetDataSource(pluginID))
assert.Nil(t, pm.GetPlugin(pluginID))
assert.Len(t, pm.StaticRoutes(), 0)
t.Run("Won't uninstall if not installed", func(t *testing.T) {
err := pm.Uninstall(context.Background(), pluginID)
require.Equal(t, plugins.ErrPluginNotInstalled, err)
})
})
})
}
func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) {
t.Helper()
panels := []string{
"alertlist",
"annolist",
"barchart",
"bargauge",
"dashlist",
"debug",
"gauge",
"gettingstarted",
"graph",
"heatmap",
"live",
"logs",
"news",
"nodeGraph",
"piechart",
"pluginlist",
"stat",
"table",
"table-old",
"text",
"timeline",
"timeseries",
"welcome",
"xychart",
}
datasources := []string{
"alertmanager",
"stackdriver",
"cloudwatch",
"dashboard",
"elasticsearch",
"grafana",
"grafana-azure-monitor-datasource",
"graphite",
"influxdb",
"jaeger",
"loki",
"mixed",
"mssql",
"mysql",
"opentsdb",
"postgres",
"prometheus",
"tempo",
"testdata",
"zipkin",
}
for _, p := range panels {
assert.NotNil(t, pm.plugins[p])
assert.NotNil(t, pm.panels[p])
}
for _, ds := range datasources {
assert.NotNil(t, pm.plugins[ds])
assert.NotNil(t, pm.dataSources[ds])
}
}
type fakeBackendPluginManager struct {
backendplugin.Manager
registeredPlugins []string
}
func (f *fakeBackendPluginManager) Register(pluginID string, factory backendplugin.PluginFactoryFunc) error {
f.registeredPlugins = append(f.registeredPlugins, pluginID)
return nil
}
func (f *fakeBackendPluginManager) RegisterAndStart(ctx context.Context, pluginID string, factory backendplugin.PluginFactoryFunc) error {
f.registeredPlugins = append(f.registeredPlugins, pluginID)
return nil
}
func (f *fakeBackendPluginManager) UnregisterAndStop(ctx context.Context, pluginID string) error {
var result []string
for _, existingPlugin := range f.registeredPlugins {
if pluginID != existingPlugin {
result = append(result, pluginID)
}
}
f.registeredPlugins = result
return nil
}
func (f *fakeBackendPluginManager) IsRegistered(pluginID string) bool {
for _, existingPlugin := range f.registeredPlugins {
if pluginID == existingPlugin {
return true
}
}
return false
}
func (f *fakeBackendPluginManager) StartPlugin(ctx context.Context, pluginID string) error {
return nil
}
func (f *fakeBackendPluginManager) CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error) {
return nil, nil
}
func (f *fakeBackendPluginManager) CheckHealth(ctx context.Context, pCtx backend.PluginContext) (*backend.CheckHealthResult, error) {
return nil, nil
}
func (f *fakeBackendPluginManager) CallResource(pluginConfig backend.PluginContext, ctx *models.ReqContext, path string) {
}
type fakePluginInstaller struct {
installCount int
uninstallCount int
}
func (f *fakePluginInstaller) Install(ctx context.Context, pluginID, version, pluginsDirectory, pluginZipURL, pluginRepoURL string) error {
f.installCount++
return nil
}
func (f *fakePluginInstaller) Uninstall(ctx context.Context, pluginID, pluginPath string) error {
f.uninstallCount++
return nil
}
func createManager(t *testing.T, cbs ...func(*PluginManager)) *PluginManager {
t.Helper()
staticRootPath, err := filepath.Abs("../../../public/")
require.NoError(t, err)
pm := newManager(&setting.Cfg{
Raw: ini.Empty(),
Env: setting.Prod,
StaticRootPath: staticRootPath,
})
pm.BackendPluginManager = &fakeBackendPluginManager{}
for _, cb := range cbs {
cb(pm)
}
return pm
}