The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/plugins/manager/manager_test.go

300 lines
10 KiB

package manager
import (
"archive/zip"
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/plugins/storage"
)
const testPluginID = "test-plugin"
func TestPluginManager_Add_Remove(t *testing.T) {
t.Run("Adding a new plugin", func(t *testing.T) {
const (
pluginID, v1 = "test-panel", "1.0.0"
zipNameV1 = "test-panel-1.0.0.zip"
pluginDirV1 = "/data/plugin/test-panel-1.0.0"
)
// mock a plugin to be returned automatically by the plugin loader
pluginV1 := createPlugin(t, pluginID, plugins.External, true, true, func(plugin *plugins.Plugin) {
plugin.Info.Version = v1
plugin.PluginDir = pluginDirV1
})
mockZipV1 := &zip.ReadCloser{Reader: zip.Reader{File: []*zip.File{{
FileHeader: zip.FileHeader{Name: zipNameV1},
}}}}
loader := &fakes.FakeLoader{
LoadFunc: func(_ context.Context, _ plugins.Class, paths []string, _ map[string]struct{}) ([]*plugins.Plugin, error) {
require.Equal(t, []string{zipNameV1}, paths)
return []*plugins.Plugin{pluginV1}, nil
},
}
pluginRepo := &fakes.FakePluginRepo{
GetPluginArchiveFunc: func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchive, error) {
require.Equal(t, pluginV1.ID, pluginID)
require.Equal(t, v1, version)
return &repo.PluginArchive{
File: mockZipV1,
}, nil
},
}
fs := &fakes.FakePluginStorage{
AddFunc: func(_ context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) {
require.Equal(t, pluginV1.ID, pluginID)
require.Equal(t, mockZipV1, z)
return &storage.ExtractedPluginArchive{
Path: zipNameV1,
}, nil
},
RegisterFunc: func(_ context.Context, pluginID, pluginDir string) error {
require.Equal(t, pluginV1.ID, pluginID)
require.Equal(t, pluginV1.PluginDir, pluginDir)
return nil
},
Added: make(map[string]string),
Removed: make(map[string]int),
}
proc := fakes.NewFakeProcessManager()
pm := New(&config.Cfg{}, fakes.NewFakePluginRegistry(), []plugins.PluginSource{}, loader, pluginRepo, fs, proc)
err := pm.Add(context.Background(), pluginID, v1, plugins.CompatOpts{})
require.NoError(t, err)
require.Equal(t, pluginV1.PluginDir, fs.Added[pluginID])
require.Equal(t, 0, fs.Removed[pluginID])
require.Equal(t, 1, proc.Started[pluginID])
require.Equal(t, 0, proc.Stopped[pluginID])
regPlugin, exists := pm.pluginRegistry.Plugin(context.Background(), pluginID)
require.True(t, exists)
require.Equal(t, pluginV1, regPlugin)
require.Len(t, pm.pluginRegistry.Plugins(context.Background()), 1)
t.Run("Won't add if already exists", func(t *testing.T) {
err = pm.Add(context.Background(), pluginID, v1, plugins.CompatOpts{})
require.Equal(t, plugins.DuplicateError{
PluginID: pluginV1.ID,
ExistingPluginDir: pluginV1.PluginDir,
}, err)
})
t.Run("Update plugin to different version", func(t *testing.T) {
const (
v2 = "2.0.0"
zipNameV2 = "test-panel-2.0.0.zip"
pluginDirV2 = "/data/plugin/test-panel-2.0.0"
)
// mock a plugin to be returned automatically by the plugin loader
pluginV2 := createPlugin(t, pluginID, plugins.External, true, true, func(plugin *plugins.Plugin) {
plugin.Info.Version = v2
plugin.PluginDir = pluginDirV2
})
mockZipV2 := &zip.ReadCloser{Reader: zip.Reader{File: []*zip.File{{
FileHeader: zip.FileHeader{Name: zipNameV2},
}}}}
loader.LoadFunc = func(_ context.Context, class plugins.Class, paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) {
require.Equal(t, plugins.External, class)
require.Empty(t, ignore)
require.Equal(t, []string{zipNameV2}, paths)
return []*plugins.Plugin{pluginV2}, nil
}
pluginRepo.GetPluginDownloadOptionsFunc = func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginDownloadOptions, error) {
return &repo.PluginDownloadOptions{
PluginZipURL: "https://grafanaplugins.com",
}, nil
}
pluginRepo.GetPluginArchiveByURLFunc = func(_ context.Context, pluginZipURL string, _ repo.CompatOpts) (*repo.PluginArchive, error) {
require.Equal(t, "https://grafanaplugins.com", pluginZipURL)
return &repo.PluginArchive{
File: mockZipV2,
}, nil
}
fs.AddFunc = func(_ context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) {
require.Equal(t, pluginV1.ID, pluginID)
require.Equal(t, mockZipV2, z)
return &storage.ExtractedPluginArchive{
Path: zipNameV2,
}, nil
}
fs.RegisterFunc = func(_ context.Context, pluginID, pluginDir string) error {
require.Equal(t, pluginV2.ID, pluginID)
require.Equal(t, pluginV2.PluginDir, pluginDir)
return nil
}
err = pm.Add(context.Background(), pluginID, v2, plugins.CompatOpts{})
require.NoError(t, err)
require.Equal(t, pluginDirV2, fs.Added[pluginID])
require.Equal(t, 1, fs.Removed[pluginID])
require.Equal(t, 2, proc.Started[pluginID])
require.Equal(t, 1, proc.Stopped[pluginID])
regPlugin, exists = pm.pluginRegistry.Plugin(context.Background(), pluginID)
require.True(t, exists)
require.Equal(t, pluginV2, regPlugin)
require.Len(t, pm.pluginRegistry.Plugins(context.Background()), 1)
})
t.Run("Removing an existing plugin", func(t *testing.T) {
err = pm.Remove(context.Background(), pluginID)
require.NoError(t, err)
require.Equal(t, 2, proc.Stopped[pluginID])
require.Equal(t, 2, fs.Removed[pluginID])
p, exists := pm.pluginRegistry.Plugin(context.Background(), pluginID)
require.False(t, exists)
require.Nil(t, p)
t.Run("Won't remove if not exists", func(t *testing.T) {
err := pm.Remove(context.Background(), pluginID)
require.Equal(t, plugins.ErrPluginNotInstalled, err)
})
})
})
t.Run("Can't update core or bundled plugin", func(t *testing.T) {
tcs := []struct {
class plugins.Class
}{
{class: plugins.Core},
{class: plugins.Bundled},
}
for _, tc := range tcs {
p := createPlugin(t, testPluginID, tc.class, true, true, func(plugin *plugins.Plugin) {
plugin.Info.Version = "1.0.0"
})
fakes.NewFakePluginRegistry()
reg := &fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
testPluginID: p,
},
}
proc := fakes.NewFakeProcessManager()
pm := New(&config.Cfg{}, reg, []plugins.PluginSource{}, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}, proc)
err := pm.Add(context.Background(), p.ID, "3.2.0", plugins.CompatOpts{})
require.ErrorIs(t, err, plugins.ErrInstallCorePlugin)
require.Equal(t, 0, proc.Started[p.ID])
require.Equal(t, 0, proc.Stopped[p.ID])
regPlugin, exists := pm.pluginRegistry.Plugin(context.Background(), testPluginID)
require.True(t, exists)
require.Equal(t, p, regPlugin)
require.Len(t, pm.pluginRegistry.Plugins(context.Background()), 1)
err = pm.Add(context.Background(), testPluginID, "", plugins.CompatOpts{})
require.Equal(t, plugins.ErrInstallCorePlugin, err)
t.Run("Can't uninstall core plugin", func(t *testing.T) {
err = pm.Remove(context.Background(), p.ID)
require.Equal(t, plugins.ErrUninstallCorePlugin, err)
})
}
})
}
func TestPluginManager_Run(t *testing.T) {
t.Run("Plugin sources are loaded in order", func(t *testing.T) {
loader := &fakes.FakeLoader{}
pm := New(&config.Cfg{}, fakes.NewFakePluginRegistry(), []plugins.PluginSource{
{Class: plugins.Bundled, Paths: []string{"path1"}},
{Class: plugins.Core, Paths: []string{"path2"}},
{Class: plugins.External, Paths: []string{"path3"}},
}, loader, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}, &fakes.FakeProcessManager{})
err := pm.Init(context.Background())
require.NoError(t, err)
require.Equal(t, []string{"path1", "path2", "path3"}, loader.LoadedPaths)
})
}
func TestManager_Renderer(t *testing.T) {
t.Run("Renderer returns a single (non-decommissioned) renderer plugin", func(t *testing.T) {
p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-renderer", Type: plugins.Renderer}}
p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel", Type: plugins.Panel}}
p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-app", Type: plugins.App}}
reg := &fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
},
}
pm := New(&config.Cfg{}, reg, []plugins.PluginSource{}, &fakes.FakeLoader{}, &fakes.FakePluginRepo{},
&fakes.FakePluginStorage{}, &fakes.FakeProcessManager{})
r := pm.Renderer(context.Background())
require.Equal(t, p1, r)
})
}
func TestManager_SecretsManager(t *testing.T) {
t.Run("Renderer returns a single (non-decommissioned) secrets manager plugin", func(t *testing.T) {
p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-renderer", Type: plugins.Renderer}}
p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel", Type: plugins.Panel}}
p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-secrets", Type: plugins.SecretsManager}}
p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.DataSource}}
reg := &fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
p4.ID: p4,
},
}
pm := New(&config.Cfg{}, reg, []plugins.PluginSource{}, &fakes.FakeLoader{}, &fakes.FakePluginRepo{},
&fakes.FakePluginStorage{}, &fakes.FakeProcessManager{})
r := pm.SecretsManager(context.Background())
require.Equal(t, p3, r)
})
}
func createPlugin(t *testing.T, pluginID string, class plugins.Class, managed, backend bool, cbs ...func(*plugins.Plugin)) *plugins.Plugin {
t.Helper()
p := &plugins.Plugin{
Class: class,
JSONData: plugins.JSONData{
ID: pluginID,
Type: plugins.DataSource,
Backend: backend,
},
}
p.SetLogger(log.NewNopLogger())
p.RegisterClient(&fakes.FakePluginClient{
ID: pluginID,
Managed: managed,
Log: p.Logger(),
})
for _, cb := range cbs {
cb(p)
}
return p
}