Plugins: Add inititialization stage to plugin loader pipeline (#72667)

* first pass

* migrate tests

* simplify

* fix comments

* fix linter

* nil checks

* remove comment
This commit is contained in:
Will Browne
2023-08-02 16:29:12 +00:00
committed by GitHub
parent 77e7ae2a1b
commit ad2705fa0b
17 changed files with 1809 additions and 1166 deletions
@@ -47,7 +47,7 @@ func New(cfg *config.Cfg, opts Opts) *Bootstrap {
opts.ConstructFunc = DefaultConstructFunc(signature.DefaultCalculator(cfg), assetpath.DefaultService(cfg))
}
if len(opts.DecorateFuncs) == 0 {
if opts.DecorateFuncs == nil {
opts.DecorateFuncs = DefaultDecorateFuncs
}
@@ -45,7 +45,7 @@ func New(cfg *config.Cfg, opts Opts) *Discovery {
opts.FindFunc = DefaultFindFunc(cfg)
}
if len(opts.FindFilterFuncs) == 0 {
if opts.FindFilterFuncs == nil {
opts.FindFilterFuncs = []FindFilterFunc{} // no filters by default
}
@@ -0,0 +1,6 @@
// Package initialization defines the fourth stage of the plugin loader pipeline.
//
// The Initialization stage must implement the Initializer interface.
// - Initialize(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error)
package initialization
@@ -0,0 +1,67 @@
package initialization
import (
"context"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log"
)
// Initializer is responsible for the Initialization stage of the plugin loader pipeline.
type Initializer interface {
Initialize(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error)
}
// InitializeFunc is the function used for the Initialize step of the Initialization stage.
type InitializeFunc func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error)
type Initialize struct {
cfg *config.Cfg
initializeSteps []InitializeFunc
log log.Logger
}
type Opts struct {
InitializeFuncs []InitializeFunc
}
// New returns a new Initialization stage.
func New(cfg *config.Cfg, opts Opts) *Initialize {
if opts.InitializeFuncs == nil {
opts.InitializeFuncs = []InitializeFunc{}
}
return &Initialize{
cfg: cfg,
initializeSteps: opts.InitializeFuncs,
log: log.New("plugins.initialization"),
}
}
// Initialize will execute the Initialize steps of the Initialization stage.
func (i *Initialize) Initialize(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) {
if len(i.initializeSteps) == 0 {
return ps, nil
}
var err error
initializedPlugins := make([]*plugins.Plugin, 0, len(ps))
for _, p := range ps {
var ip *plugins.Plugin
stepFailed := false
for _, init := range i.initializeSteps {
ip, err = init(ctx, p)
if err != nil {
stepFailed = true
i.log.Error("Could not initialize plugin", "pluginId", p.ID, "err", err)
break
}
}
if !stepFailed {
initializedPlugins = append(initializedPlugins, ip)
}
}
return initializedPlugins, nil
}
@@ -0,0 +1,90 @@
package initialization
import (
"context"
"errors"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/envvars"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
)
// BackendClientInit implements an InitializeFunc for initializing a backend plugin process.
//
// It uses the envvars.Provider to retrieve the environment variables required for the plugin and the plugins.BackendFactoryProvider
// to get fetch backend factory, which is used to form a connection to the backend plugin process.
//
// Note: This step does not start the backend plugin process.
type BackendClientInit struct {
envVarProvider envvars.Provider
backendProvider plugins.BackendFactoryProvider
log log.Logger
}
// NewBackendClientInitStep returns a new InitializeFunc for registering a backend plugin process.
func NewBackendClientInitStep(envVarProvider envvars.Provider,
backendProvider plugins.BackendFactoryProvider) InitializeFunc {
return newBackendProcessRegistration(envVarProvider, backendProvider).Initialize
}
func newBackendProcessRegistration(envVarProvider envvars.Provider,
backendProvider plugins.BackendFactoryProvider) *BackendClientInit {
return &BackendClientInit{
backendProvider: backendProvider,
envVarProvider: envVarProvider,
log: log.New("plugins.backend.registration"),
}
}
// Initialize will initialize a backend plugin client, if the plugin is a backend plugin.
func (b *BackendClientInit) Initialize(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
if p.Backend {
backendFactory := b.backendProvider.BackendFactory(ctx, p)
if backendFactory == nil {
return nil, errors.New("could not find backend factory for plugin")
}
env, err := b.envVarProvider.Get(ctx, p)
if err != nil {
return nil, err
}
if backendClient, err := backendFactory(p.ID, p.Logger(), env); err != nil {
return nil, err
} else {
p.RegisterClient(backendClient)
}
}
return p, nil
}
// PluginRegistration implements an InitializeFunc for registering a plugin with the plugin registry.
type PluginRegistration struct {
pluginRegistry registry.Service
log log.Logger
}
// NewPluginRegistrationStep returns a new InitializeFunc for registering a plugin with the plugin registry.
func NewPluginRegistrationStep(pluginRegistry registry.Service) InitializeFunc {
return newPluginRegistration(pluginRegistry).Initialize
}
func newPluginRegistration(pluginRegistry registry.Service) *PluginRegistration {
return &PluginRegistration{
pluginRegistry: pluginRegistry,
log: log.New("plugins.registration"),
}
}
// Initialize registers the plugin with the plugin registry.
func (r *PluginRegistration) Initialize(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
if err := r.pluginRegistry.Add(ctx, p); err != nil {
r.log.Error("Could not register plugin", "pluginID", p.ID, "err", err)
return nil, errors.New("could not register plugin")
}
if !p.IsCorePlugin() {
r.log.Info("Plugin registered", "pluginID", p.ID)
}
return p, nil
}
@@ -0,0 +1,132 @@
package initialization
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/log"
)
func TestInitializer_Initialize(t *testing.T) {
t.Run("core backend datasource", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "test",
Type: plugins.TypeDataSource,
Includes: []*plugins.Includes{
{
Name: "Example dashboard",
Type: plugins.TypeDashboard,
},
},
Backend: true,
},
Class: plugins.ClassCore,
}
stepFunc := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p})
var err error
p, err = stepFunc(context.Background(), p)
require.NoError(t, err)
c, exists := p.Client()
require.True(t, exists)
require.NotNil(t, c)
})
t.Run("renderer", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "test",
Type: plugins.TypeRenderer,
Dependencies: plugins.Dependencies{
GrafanaVersion: ">=8.x",
},
Backend: true,
},
Class: plugins.ClassExternal,
}
stepFunc := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p})
var err error
p, err = stepFunc(context.Background(), p)
require.NoError(t, err)
c, exists := p.Client()
require.True(t, exists)
require.NotNil(t, c)
})
t.Run("secretsmanager", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "test",
Type: plugins.TypeSecretsManager,
Dependencies: plugins.Dependencies{
GrafanaVersion: ">=8.x",
},
Backend: true,
},
Class: plugins.ClassExternal,
}
stepFunc := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p})
var err error
p, err = stepFunc(context.Background(), p)
require.NoError(t, err)
c, exists := p.Client()
require.True(t, exists)
require.NotNil(t, c)
})
t.Run("non backend plugin app", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
Backend: false,
},
}
i := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{
plugin: p,
})
var err error
p, err = i(context.Background(), p)
require.NoError(t, err)
c, exists := p.Client()
require.False(t, exists)
require.Nil(t, c)
})
}
type fakeBackendProvider struct {
plugins.BackendFactoryProvider
plugin *plugins.Plugin
}
func (f *fakeBackendProvider) BackendFactory(_ context.Context, _ *plugins.Plugin) backendplugin.PluginFactoryFunc {
return func(_ string, _ log.Logger, _ []string) (backendplugin.Plugin, error) {
return f.plugin, nil
}
}
type fakeEnvVarsProvider struct {
GetFunc func(ctx context.Context, p *plugins.Plugin) []string
}
func (f *fakeEnvVarsProvider) Get(ctx context.Context, p *plugins.Plugin) ([]string, error) {
if f.GetFunc != nil {
return f.GetFunc(ctx, p), nil
}
return nil, nil
}