Plugins: Add validation stage to plugin loader pipeline (#73053)

* first pass

* change validation signature

* err tracking

* fix

* undo golden

* 1 more

* fix

* adjust doc

* add test helper

* fix linter
This commit is contained in:
Will Browne
2023-08-09 18:25:28 +02:00
committed by GitHub
parent 69c8200fc9
commit 72da44db0e
23 changed files with 624 additions and 368 deletions

View File

@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
@ -19,27 +18,14 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client" pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client"
"github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
"github.com/grafana/grafana/pkg/plugins/manager/sources"
"github.com/grafana/grafana/pkg/plugins/manager/store"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/caching" "github.com/grafana/grafana/pkg/services/caching"
datasources "github.com/grafana/grafana/pkg/services/datasources/fakes" datasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest"
"github.com/grafana/grafana/pkg/services/pluginsintegration" "github.com/grafana/grafana/pkg/services/pluginsintegration"
"github.com/grafana/grafana/pkg/services/pluginsintegration/config"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
@ -66,36 +52,18 @@ func TestCallResource(t *testing.T) {
coreRegistry := coreplugin.ProvideCoreRegistry(nil, &cloudwatch.CloudWatchService{}, nil, nil, nil, nil, coreRegistry := coreplugin.ProvideCoreRegistry(nil, &cloudwatch.CloudWatchService{}, nil, nil, nil, nil,
nil, nil, nil, nil, testdatasource.ProvideService(), nil, nil, nil, nil, nil, nil) nil, nil, nil, nil, testdatasource.ProvideService(), nil, nil, nil, nil, nil, nil)
pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures())
require.NoError(t, err)
reg := registry.ProvideService()
angularInspector, err := angularinspector.NewStaticInspector()
require.NoError(t, err)
proc := process.NewManager(reg)
discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) textCtx := pluginsintegration.CreateIntegrationTestCtx(t, cfg, coreRegistry)
bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)))
initialize := pipeline.ProvideInitializationStage(pCfg, reg, fakes.NewFakeLicensingService(), provider.ProvideService(coreRegistry), proc, &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry())
terminate, err := pipeline.ProvideTerminationStage(pCfg, reg, proc)
require.NoError(t, err)
l := loader.ProvideService(pCfg, signature.NewUnsignedAuthorizer(pCfg), pcp := plugincontext.ProvideService(localcache.ProvideService(), textCtx.PluginStore, &datasources.FakeDataSourceService{},
reg, fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)),
angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize, terminate)
srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l)
require.NoError(t, err)
pcp := plugincontext.ProvideService(localcache.ProvideService(), ps, &datasources.FakeDataSourceService{},
pluginSettings.ProvideService(db.InitTestDB(t), fakeSecrets.NewFakeSecretsService())) pluginSettings.ProvideService(db.InitTestDB(t), fakeSecrets.NewFakeSecretsService()))
srv := SetupAPITestServer(t, func(hs *HTTPServer) { srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg hs.Cfg = cfg
hs.pluginContextProvider = pcp hs.pluginContextProvider = pcp
hs.QuotaService = quotatest.New(false, nil) hs.QuotaService = quotatest.New(false, nil)
hs.pluginStore = ps hs.pluginStore = textCtx.PluginStore
hs.pluginClient = pluginClient.ProvideService(reg, pCfg) hs.pluginClient = textCtx.PluginClient
hs.log = log.New("test") hs.log = log.New("test")
}) })
@ -133,7 +101,7 @@ func TestCallResource(t *testing.T) {
hs.Cfg = cfg hs.Cfg = cfg
hs.pluginContextProvider = pcp hs.pluginContextProvider = pcp
hs.QuotaService = quotatest.New(false, nil) hs.QuotaService = quotatest.New(false, nil)
hs.pluginStore = ps hs.pluginStore = textCtx.PluginStore
hs.pluginClient = pc hs.pluginClient = pc
hs.log = log.New("test") hs.log = log.New("test")
}) })

View File

@ -486,6 +486,17 @@ func (f *FakeBootstrapper) Bootstrap(ctx context.Context, src plugins.PluginSour
return []*plugins.Plugin{}, nil return []*plugins.Plugin{}, nil
} }
type FakeValidator struct {
ValidateFunc func(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error)
}
func (f *FakeValidator) Validate(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) {
if f.ValidateFunc != nil {
return f.ValidateFunc(ctx, ps)
}
return []*plugins.Plugin{}, nil
}
type FakeInitializer struct { type FakeInitializer struct {
IntializeFunc func(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) IntializeFunc func(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error)
} }

View File

@ -73,6 +73,6 @@ func NewDefaultStaticDetectorsProvider() angulardetector.DetectorsProvider {
// NewStaticInspector returns the default Inspector, which is a PatternsListInspector that only uses the // NewStaticInspector returns the default Inspector, which is a PatternsListInspector that only uses the
// static (hardcoded) angular detection patterns. // static (hardcoded) angular detection patterns.
func NewStaticInspector() (Inspector, error) { func NewStaticInspector() Inspector {
return &PatternsListInspector{DetectorsProvider: NewDefaultStaticDetectorsProvider()}, nil return &PatternsListInspector{DetectorsProvider: NewDefaultStaticDetectorsProvider()}
} }

View File

@ -2,154 +2,63 @@ package loader
import ( import (
"context" "context"
"errors"
"time"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination"
"github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/oauth"
) )
var _ plugins.ErrorResolver = (*Loader)(nil)
type Loader struct { type Loader struct {
discovery discovery.Discoverer discovery discovery.Discoverer
bootstrap bootstrap.Bootstrapper bootstrap bootstrap.Bootstrapper
initializer initialization.Initializer initializer initialization.Initializer
termination termination.Terminator termination termination.Terminator
validation validation.Validator
processManager process.Service log log.Logger
pluginRegistry registry.Service
roleRegistry plugins.RoleRegistry
signatureValidator signature.Validator
externalServiceRegistry oauth.ExternalServiceRegistry
assetPath *assetpath.Service
log log.Logger
cfg *config.Cfg
angularInspector angularinspector.Inspector
errs map[string]*plugins.SignatureError
} }
func ProvideService(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, func ProvideService(discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, validation validation.Validator,
pluginRegistry registry.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, initializer initialization.Initializer, termination termination.Terminator) *Loader {
angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, return New(discovery, bootstrap, validation, initializer, termination)
discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer,
termination termination.Terminator) *Loader {
return New(cfg, authorizer, pluginRegistry, process.NewManager(pluginRegistry), roleRegistry, assetPath,
angularInspector, externalServiceRegistry, discovery, bootstrap, initializer, termination)
} }
func New(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, func New(
processManager process.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, validation validation.Validator,
angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, initializer initialization.Initializer, termination termination.Terminator) *Loader {
discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer,
termination termination.Terminator) *Loader {
return &Loader{ return &Loader{
pluginRegistry: pluginRegistry, discovery: discovery,
signatureValidator: signature.NewValidator(authorizer), bootstrap: bootstrap,
processManager: processManager, validation: validation,
errs: make(map[string]*plugins.SignatureError), initializer: initializer,
log: log.New("plugin.loader"), termination: termination,
roleRegistry: roleRegistry, log: log.New("plugin.loader"),
cfg: cfg,
assetPath: assetPath,
angularInspector: angularInspector,
externalServiceRegistry: externalServiceRegistry,
discovery: discovery,
bootstrap: bootstrap,
initializer: initializer,
termination: termination,
} }
} }
func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) {
// <DISCOVERY STAGE>
discoveredPlugins, err := l.discovery.Discover(ctx, src) discoveredPlugins, err := l.discovery.Discover(ctx, src)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// </DISCOVERY STAGE>
// <BOOTSTRAP STAGE>
bootstrappedPlugins, err := l.bootstrap.Bootstrap(ctx, src, discoveredPlugins) bootstrappedPlugins, err := l.bootstrap.Bootstrap(ctx, src, discoveredPlugins)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// </BOOTSTRAP STAGE>
// <VERIFICATION STAGE> verifiedPlugins, err := l.validation.Validate(ctx, bootstrappedPlugins)
verifiedPlugins := make([]*plugins.Plugin, 0, len(bootstrappedPlugins)) if err != nil {
for _, plugin := range bootstrappedPlugins { return nil, err
signingError := l.signatureValidator.Validate(plugin)
if signingError != nil {
l.log.Warn("Skipping loading plugin due to problem with signature",
"pluginID", plugin.ID, "status", signingError.SignatureStatus)
plugin.SignatureError = signingError
l.errs[plugin.ID] = signingError
// skip plugin so it will not be loaded any further
continue
}
// clear plugin error if a pre-existing error has since been resolved
delete(l.errs, plugin.ID)
// verify module.js exists for SystemJS to load.
// CDN plugins can be loaded with plugin.json only, so do not warn for those.
if !plugin.IsRenderer() && !plugin.IsCorePlugin() {
f, err := plugin.FS.Open("module.js")
if err != nil {
if errors.Is(err, plugins.ErrFileNotExist) {
l.log.Warn("Plugin missing module.js", "pluginID", plugin.ID,
"warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.")
}
} else if f != nil {
if err := f.Close(); err != nil {
l.log.Warn("Could not close module.js", "pluginID", plugin.ID, "err", err)
}
}
}
// detect angular for external plugins
if plugin.IsExternalPlugin() {
var err error
cctx, canc := context.WithTimeout(ctx, time.Second*10)
plugin.AngularDetected, err = l.angularInspector.Inspect(cctx, plugin)
canc()
if err != nil {
l.log.Warn("Could not inspect plugin for angular", "pluginID", plugin.ID, "err", err)
}
// Do not initialize plugins if they're using Angular and Angular support is disabled
if plugin.AngularDetected && !l.cfg.AngularSupportEnabled {
l.log.Error("Refusing to initialize plugin because it's using Angular, which has been disabled", "pluginID", plugin.ID)
continue
}
}
verifiedPlugins = append(verifiedPlugins, plugin)
} }
// </VERIFICATION STAGE>
// <INITIALIZATION STAGE>
initializedPlugins, err := l.initializer.Initialize(ctx, verifiedPlugins) initializedPlugins, err := l.initializer.Initialize(ctx, verifiedPlugins)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// </INITIALIZATION STAGE>
return initializedPlugins, nil return initializedPlugins, nil
} }
@ -157,15 +66,3 @@ func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins
func (l *Loader) Unload(ctx context.Context, pluginID string) error { func (l *Loader) Unload(ctx context.Context, pluginID string) error {
return l.termination.Terminate(ctx, pluginID) return l.termination.Terminate(ctx, pluginID)
} }
func (l *Loader) PluginErrors() []*plugins.Error {
errs := make([]*plugins.Error, 0, len(l.errs))
for _, err := range l.errs {
errs = append(errs, &plugins.Error{
PluginID: err.PluginID,
ErrorCode: err.AsErrorCode(),
})
}
return errs
}

View File

@ -14,15 +14,12 @@ import (
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation"
"github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/manager/sources"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
) )
@ -61,12 +58,11 @@ func TestLoader_Load(t *testing.T) {
return return
} }
tests := []struct { tests := []struct {
name string name string
class plugins.Class class plugins.Class
cfg *config.Cfg cfg *config.Cfg
pluginPaths []string pluginPaths []string
want []*plugins.Plugin want []*plugins.Plugin
pluginErrors map[string]*plugins.Error
}{ }{
{ {
name: "Load a Core plugin", name: "Load a Core plugin",
@ -272,18 +268,13 @@ func TestLoader_Load(t *testing.T) {
Signature: "unsigned", Signature: "unsigned",
}, },
}, },
}, { },
{
name: "Load an unsigned plugin (production)", name: "Load an unsigned plugin (production)",
class: plugins.ClassExternal, class: plugins.ClassExternal,
cfg: &config.Cfg{}, cfg: &config.Cfg{},
pluginPaths: []string{"../testdata/unsigned-datasource"}, pluginPaths: []string{"../testdata/unsigned-datasource"},
want: []*plugins.Plugin{}, want: []*plugins.Plugin{},
pluginErrors: map[string]*plugins.Error{
"test-datasource": {
PluginID: "test-datasource",
ErrorCode: "signatureMissing",
},
},
}, },
{ {
name: "Load an unsigned plugin using PluginsAllowUnsigned config (production)", name: "Load an unsigned plugin using PluginsAllowUnsigned config (production)",
@ -330,12 +321,6 @@ func TestLoader_Load(t *testing.T) {
cfg: &config.Cfg{}, cfg: &config.Cfg{},
pluginPaths: []string{"../testdata/lacking-files"}, pluginPaths: []string{"../testdata/lacking-files"},
want: []*plugins.Plugin{}, want: []*plugins.Plugin{},
pluginErrors: map[string]*plugins.Error{
"test-datasource": {
PluginID: "test-datasource",
ErrorCode: "signatureInvalid",
},
},
}, },
{ {
name: "Load a plugin with v1 manifest using PluginsAllowUnsigned config (production) should return signatureInvali", name: "Load a plugin with v1 manifest using PluginsAllowUnsigned config (production) should return signatureInvali",
@ -345,12 +330,6 @@ func TestLoader_Load(t *testing.T) {
}, },
pluginPaths: []string{"../testdata/lacking-files"}, pluginPaths: []string{"../testdata/lacking-files"},
want: []*plugins.Plugin{}, want: []*plugins.Plugin{},
pluginErrors: map[string]*plugins.Error{
"test-datasource": {
PluginID: "test-datasource",
ErrorCode: "signatureInvalid",
},
},
}, },
{ {
name: "Load a plugin with manifest which has a file not found in plugin folder", name: "Load a plugin with manifest which has a file not found in plugin folder",
@ -360,12 +339,6 @@ func TestLoader_Load(t *testing.T) {
}, },
pluginPaths: []string{"../testdata/invalid-v2-missing-file"}, pluginPaths: []string{"../testdata/invalid-v2-missing-file"},
want: []*plugins.Plugin{}, want: []*plugins.Plugin{},
pluginErrors: map[string]*plugins.Error{
"test-datasource": {
PluginID: "test-datasource",
ErrorCode: "signatureModified",
},
},
}, },
{ {
name: "Load a plugin with file which is missing from the manifest", name: "Load a plugin with file which is missing from the manifest",
@ -375,12 +348,6 @@ func TestLoader_Load(t *testing.T) {
}, },
pluginPaths: []string{"../testdata/invalid-v2-extra-file"}, pluginPaths: []string{"../testdata/invalid-v2-extra-file"},
want: []*plugins.Plugin{}, want: []*plugins.Plugin{},
pluginErrors: map[string]*plugins.Error{
"test-datasource": {
PluginID: "test-datasource",
ErrorCode: "signatureModified",
},
},
}, },
{ {
name: "Load an app with includes", name: "Load an app with includes",
@ -434,31 +401,19 @@ func TestLoader_Load(t *testing.T) {
}, },
} }
for _, tt := range tests { for _, tt := range tests {
angularInspector, err := angularinspector.NewStaticInspector()
require.NoError(t, err)
terminationStage, err := termination.New(tt.cfg, termination.Opts{})
require.NoError(t, err)
l := New(tt.cfg, signature.NewUnsignedAuthorizer(tt.cfg), fakes.NewFakePluginRegistry(),
fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(tt.cfg)), angularInspector, &fakes.FakeOauthService{},
discovery.New(tt.cfg, discovery.Opts{}), bootstrap.New(tt.cfg, bootstrap.Opts{}),
initialization.New(tt.cfg, initialization.Opts{}),
terminationStage)
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
terminationStage, err := termination.New(tt.cfg, termination.Opts{})
require.NoError(t, err)
l := New(discovery.New(tt.cfg, discovery.Opts{}), bootstrap.New(tt.cfg, bootstrap.Opts{}),
validation.New(tt.cfg, validation.Opts{}), initialization.New(tt.cfg, initialization.Opts{}),
terminationStage)
got, err := l.Load(context.Background(), sources.NewLocalSource(tt.class, tt.pluginPaths)) got, err := l.Load(context.Background(), sources.NewLocalSource(tt.class, tt.pluginPaths))
require.NoError(t, err) require.NoError(t, err)
if !cmp.Equal(got, tt.want, compareOpts...) { if !cmp.Equal(got, tt.want, compareOpts...) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...)) t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...))
} }
pluginErrs := l.PluginErrors()
require.Equal(t, len(tt.pluginErrors), len(pluginErrs))
for _, pluginErr := range pluginErrs {
require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr)
}
}) })
} }
@ -474,41 +429,41 @@ func TestLoader_Load(t *testing.T) {
return plugins.Signature{}, false return plugins.Signature{}, false
}, },
} }
pJSON := plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource, Info: plugins.Info{Version: "1.0.0"}} pluginJSON := plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource, Info: plugins.Info{Version: "1.0.0"}}
p := &plugins.Plugin{ plugin := &plugins.Plugin{
JSONData: pJSON, JSONData: pluginJSON,
Signature: plugins.SignatureStatusValid, Signature: plugins.SignatureStatusValid,
SignatureType: plugins.SignatureTypeCommunity, SignatureType: plugins.SignatureTypeCommunity,
FS: plugins.NewFakeFS(), FS: plugins.NewFakeFS(),
} }
cfg := &config.Cfg{}
angularInspector, err := angularinspector.NewStaticInspector()
require.NoError(t, err)
var steps []string var steps []string
l := New(cfg, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), l := New(
fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)),
angularInspector, &fakes.FakeOauthService{},
&fakes.FakeDiscoverer{ &fakes.FakeDiscoverer{
DiscoverFunc: func(ctx context.Context, s plugins.PluginSource) ([]*plugins.FoundBundle, error) { DiscoverFunc: func(ctx context.Context, s plugins.PluginSource) ([]*plugins.FoundBundle, error) {
require.Equal(t, src, s) require.Equal(t, src, s)
steps = append(steps, "discover") steps = append(steps, "discover")
return []*plugins.FoundBundle{{Primary: plugins.FoundPlugin{JSONData: pJSON}}}, nil return []*plugins.FoundBundle{{Primary: plugins.FoundPlugin{JSONData: pluginJSON}}}, nil
}, },
}, &fakes.FakeBootstrapper{ }, &fakes.FakeBootstrapper{
BootstrapFunc: func(ctx context.Context, s plugins.PluginSource, b []*plugins.FoundBundle) ([]*plugins.Plugin, error) { BootstrapFunc: func(ctx context.Context, s plugins.PluginSource, b []*plugins.FoundBundle) ([]*plugins.Plugin, error) {
require.True(t, len(b) == 1) require.True(t, len(b) == 1)
require.Equal(t, b[0].Primary.JSONData, pJSON) require.Equal(t, b[0].Primary.JSONData, pluginJSON)
require.Equal(t, src, s) require.Equal(t, src, s)
steps = append(steps, "bootstrap") steps = append(steps, "bootstrap")
return []*plugins.Plugin{p}, nil return []*plugins.Plugin{plugin}, nil
}, },
}, &fakes.FakeInitializer{ }, &fakes.FakeValidator{ValidateFunc: func(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) {
require.Equal(t, []*plugins.Plugin{plugin}, ps)
steps = append(steps, "validate")
return ps, nil
}},
&fakes.FakeInitializer{
IntializeFunc: func(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) { IntializeFunc: func(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) {
require.True(t, len(ps) == 1) require.True(t, len(ps) == 1)
require.Equal(t, ps[0].JSONData, pJSON) require.Equal(t, ps[0].JSONData, pluginJSON)
steps = append(steps, "initialize") steps = append(steps, "initialize")
return ps, nil return ps, nil
}, },
@ -516,19 +471,14 @@ func TestLoader_Load(t *testing.T) {
got, err := l.Load(context.Background(), src) got, err := l.Load(context.Background(), src)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []*plugins.Plugin{p}, got) require.Equal(t, []*plugins.Plugin{plugin}, got)
require.Equal(t, []string{"discover", "bootstrap", "initialize"}, steps) require.Equal(t, []string{"discover", "bootstrap", "validate", "initialize"}, steps)
require.Zero(t, len(l.PluginErrors()))
}) })
} }
func TestLoader_Unload(t *testing.T) { func TestLoader_Unload(t *testing.T) {
t.Run("Termination stage error is returned from Unload", func(t *testing.T) { t.Run("Termination stage error is returned from Unload", func(t *testing.T) {
pluginID := "grafana-test-panel" pluginID := "grafana-test-panel"
cfg := &config.Cfg{}
angularInspector, err := angularinspector.NewStaticInspector()
require.NoError(t, err)
tcs := []struct { tcs := []struct {
expectedErr error expectedErr error
}{ }{
@ -541,9 +491,10 @@ func TestLoader_Unload(t *testing.T) {
} }
for _, tc := range tcs { for _, tc := range tcs {
l := New(cfg, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), l := New(&fakes.FakeDiscoverer{},
fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), angularInspector, &fakes.FakeBootstrapper{},
&fakes.FakeOauthService{}, &fakes.FakeDiscoverer{}, &fakes.FakeBootstrapper{}, &fakes.FakeInitializer{}, &fakes.FakeValidator{},
&fakes.FakeInitializer{},
&fakes.FakeTerminator{ &fakes.FakeTerminator{
TerminateFunc: func(ctx context.Context, pID string) error { TerminateFunc: func(ctx context.Context, pID string) error {
require.Equal(t, pluginID, pID) require.Equal(t, pluginID, pID)
@ -551,7 +502,7 @@ func TestLoader_Unload(t *testing.T) {
}, },
}) })
err = l.Unload(context.Background(), pluginID) err := l.Unload(context.Background(), pluginID)
require.ErrorIs(t, err, tc.expectedErr) require.ErrorIs(t, err, tc.expectedErr)
} }
}) })

View File

@ -1,6 +1,5 @@
// Package bootstrap defines the Bootstrap stage of the plugin loader pipeline. // Package bootstrap defines the Bootstrap stage of the plugin loader pipeline.
// //
// The Bootstrap stage must implement the Bootstrapper interface. // The Bootstrap stage must implement the Bootstrapper interface.
// - Bootstrap(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error)
package bootstrap package bootstrap

View File

@ -1,6 +1,5 @@
// Package discovery defines the Discovery stage of the plugin loader pipeline. // Package discovery defines the Discovery stage of the plugin loader pipeline.
// The Discovery stage must implement the Discoverer interface. // The Discovery stage must implement the Discoverer interface.
// - Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error)
package discovery package discovery

View File

@ -4,7 +4,7 @@
// A plugin loader pipeline is defined by the following stages: // A plugin loader pipeline is defined by the following stages:
// Discovery: Find plugins (e.g. from disk, remote, etc.), and [optionally] filter the results based on some criteria. // Discovery: Find plugins (e.g. from disk, remote, etc.), and [optionally] filter the results based on some criteria.
// Bootstrap: Create the plugins found in the discovery stage and enrich them with metadata. // Bootstrap: Create the plugins found in the discovery stage and enrich them with metadata.
// Verification: Verify the plugins based on some criteria (e.g. signature validation, angular detection, etc.) // Validation: Validate the plugins based on some criteria (e.g. signature, angular, etc.)
// Initialization: Initialize the plugin for use (e.g. register with Grafana, start the backend process, declare RBAC roles etc.) // Initialization: Initialize the plugin for use (e.g. register with Grafana, start the backend process, declare RBAC roles etc.)
// - Termination: Terminate the plugin (e.g. stop the backend process, cleanup, etc.) // - Termination: Terminate the plugin (e.g. stop the backend process, cleanup, etc.)

View File

@ -1,6 +1,5 @@
// Package initialization defines the Initialization stage of the plugin loader pipeline. // Package initialization defines the Initialization stage of the plugin loader pipeline.
// //
// The Initialization stage must implement the Initializer interface. // The Initialization stage must implement the Initializer interface.
// - Initialize(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error)
package initialization package initialization

View File

@ -1,5 +1,4 @@
// Package termination defines the Termination stage of the plugin loader pipeline. // Package termination defines the Termination stage of the plugin loader pipeline.
// //
// The Termination stage must implement the Terminator interface. // The Termination stage must implement the Terminator interface.
// - Terminate(ctx context.Context, pluginID string) error
package termination package termination

View File

@ -0,0 +1,5 @@
// Package validation defines the Validation stage of the plugin loader pipeline.
//
// The Validation stage must implement the Validator interface.
package validation

View File

@ -0,0 +1,113 @@
package validation
import (
"context"
"errors"
"time"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
)
// DefaultValidateFuncs are the default ValidateFunc used for the Validate step of the Validation stage.
func DefaultValidateFuncs(cfg *config.Cfg) []ValidateFunc {
return []ValidateFunc{
SignatureValidationStep(signature.NewValidator(signature.NewUnsignedAuthorizer(cfg))),
ModuleJSValidationStep(),
AngularDetectionStep(cfg, angularinspector.NewStaticInspector()),
}
}
type PluginSignatureValidator struct {
signatureValidator signature.Validator
log log.Logger
}
func SignatureValidationStep(signatureValidator signature.Validator) ValidateFunc {
return newPluginSignatureValidator(signatureValidator).Validate
}
func newPluginSignatureValidator(signatureValidator signature.Validator) *PluginSignatureValidator {
return &PluginSignatureValidator{
signatureValidator: signatureValidator,
log: log.New("plugins.validator.signature"),
}
}
func (v *PluginSignatureValidator) Validate(_ context.Context, p *plugins.Plugin) error {
return v.signatureValidator.ValidateSignature(p)
}
type ModuleJSValidator struct {
log log.Logger
}
func ModuleJSValidationStep() ValidateFunc {
return newModuleJSValidator().Validate
}
func newModuleJSValidator() *ModuleJSValidator {
return &ModuleJSValidator{
log: log.New("plugins.validator.module"),
}
}
func (v *ModuleJSValidator) Validate(_ context.Context, p *plugins.Plugin) error {
if !p.IsRenderer() && !p.IsCorePlugin() {
f, err := p.FS.Open("module.js")
if err != nil {
if errors.Is(err, plugins.ErrFileNotExist) {
v.log.Warn("Plugin missing module.js", "pluginID", p.ID,
"warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.")
}
} else if f != nil {
if err = f.Close(); err != nil {
v.log.Warn("Could not close module.js", "pluginID", p.ID, "err", err)
}
}
}
return nil
}
type AngularDetector struct {
cfg *config.Cfg
angularInspector angularinspector.Inspector
log log.Logger
}
func AngularDetectionStep(cfg *config.Cfg, angularInspector angularinspector.Inspector) ValidateFunc {
return newAngularDetector(cfg, angularInspector).Validate
}
func newAngularDetector(cfg *config.Cfg, angularInspector angularinspector.Inspector) *AngularDetector {
return &AngularDetector{
cfg: cfg,
angularInspector: angularInspector,
log: log.New("plugins.validator.angular"),
}
}
func (a *AngularDetector) Validate(ctx context.Context, p *plugins.Plugin) error {
if p.IsExternalPlugin() {
var err error
cctx, canc := context.WithTimeout(ctx, time.Second*10)
p.AngularDetected, err = a.angularInspector.Inspect(cctx, p)
canc()
if err != nil {
a.log.Warn("Could not inspect plugin for angular", "pluginID", p.ID, "err", err)
}
// Do not initialize plugins if they're using Angular and Angular support is disabled
if p.AngularDetected && !a.cfg.AngularSupportEnabled {
a.log.Error("Refusing to initialize plugin because it's using Angular, which has been disabled", "pluginID", p.ID)
return errors.New("angular plugins are not supported")
}
}
return nil
}

View File

@ -0,0 +1,67 @@
package validation
import (
"context"
"errors"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log"
)
// Validator is responsible for the Validation stage of the plugin loader pipeline.
type Validator interface {
Validate(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error)
}
// ValidateFunc is the function used for the Validate step of the Validation stage.
type ValidateFunc func(ctx context.Context, p *plugins.Plugin) error
type Validate struct {
cfg *config.Cfg
validateSteps []ValidateFunc
log log.Logger
}
type Opts struct {
ValidateFuncs []ValidateFunc
}
// New returns a new Validation stage.
func New(cfg *config.Cfg, opts Opts) *Validate {
if opts.ValidateFuncs == nil {
opts.ValidateFuncs = DefaultValidateFuncs(cfg)
}
return &Validate{
cfg: cfg,
validateSteps: opts.ValidateFuncs,
log: log.New("plugins.validation"),
}
}
// Validate will execute the Validate steps of the Validation stage.
func (t *Validate) Validate(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) {
if len(t.validateSteps) == 0 {
return ps, nil
}
var err error
verifiedPlugins := make([]*plugins.Plugin, 0, len(ps))
for _, p := range ps {
stepFailed := false
for _, validate := range t.validateSteps {
err = validate(ctx, p)
if err != nil && !errors.Is(err, nil) {
stepFailed = true
t.log.Error("Plugin verification failed", "pluginID", p.ID, "err", err)
break
}
}
if !stepFailed {
verifiedPlugins = append(verifiedPlugins, p)
}
}
return verifiedPlugins, nil
}

View File

@ -5,19 +5,27 @@ import (
"github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/log"
) )
type Validator struct { type Validator interface {
ValidateSignature(plugin *plugins.Plugin) error
}
type Validation struct {
authorizer plugins.PluginLoaderAuthorizer authorizer plugins.PluginLoaderAuthorizer
log log.Logger log log.Logger
} }
func NewValidator(authorizer plugins.PluginLoaderAuthorizer) Validator { func ProvideValidatorService(authorizer plugins.PluginLoaderAuthorizer) *Validation {
return Validator{ return NewValidator(authorizer)
}
func NewValidator(authorizer plugins.PluginLoaderAuthorizer) *Validation {
return &Validation{
authorizer: authorizer, authorizer: authorizer,
log: log.New("plugin.signature.validator"), log: log.New("plugin.signature.validator"),
} }
} }
func (s *Validator) Validate(plugin *plugins.Plugin) *plugins.SignatureError { func (s *Validation) ValidateSignature(plugin *plugins.Plugin) error {
if plugin.Signature.IsValid() { if plugin.Signature.IsValid() {
s.log.Debug("Plugin has valid signature", "id", plugin.ID) s.log.Debug("Plugin has valid signature", "id", plugin.ID)
return nil return nil

View File

@ -0,0 +1,38 @@
package loader
import (
"context"
"github.com/grafana/grafana/pkg/plugins"
)
type fakeSignatureErrorTracker struct {
RecordFunc func(ctx context.Context, err *plugins.SignatureError)
ClearFunc func(ctx context.Context, pluginID string)
SignatureErrorsFunc func(ctx context.Context) []*plugins.SignatureError
}
func newFakeSignatureErrorTracker() *fakeSignatureErrorTracker {
return &fakeSignatureErrorTracker{}
}
func (t *fakeSignatureErrorTracker) Record(ctx context.Context, err *plugins.SignatureError) {
if t.RecordFunc != nil {
t.RecordFunc(ctx, err)
return
}
}
func (t *fakeSignatureErrorTracker) Clear(ctx context.Context, pluginID string) {
if t.ClearFunc != nil {
t.ClearFunc(ctx, pluginID)
return
}
}
func (t *fakeSignatureErrorTracker) SignatureErrors(ctx context.Context) []*plugins.SignatureError {
if t.SignatureErrorsFunc != nil {
return t.SignatureErrorsFunc(ctx)
}
return nil
}

View File

@ -4,35 +4,25 @@ import (
"context" "context"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
pluginsLoader "github.com/grafana/grafana/pkg/plugins/manager/loader" pluginsLoader "github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination"
"github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/oauth"
) )
var _ plugins.ErrorResolver = (*Loader)(nil)
var _ pluginsLoader.Service = (*Loader)(nil) var _ pluginsLoader.Service = (*Loader)(nil)
type Loader struct { type Loader struct {
loader *pluginsLoader.Loader loader *pluginsLoader.Loader
} }
func ProvideService(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, processManager process.Service, func ProvideService(discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, validation validation.Validator,
pluginRegistry registry.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, initializer initialization.Initializer, termination termination.Terminator,
angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry,
discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer,
termination termination.Terminator,
) *Loader { ) *Loader {
return &Loader{ return &Loader{
loader: pluginsLoader.New(cfg, authorizer, pluginRegistry, processManager, roleRegistry, assetPath, loader: pluginsLoader.New(discovery, bootstrap, validation, initializer, termination),
angularInspector, externalServiceRegistry, discovery, bootstrap, initializer, termination),
} }
} }
@ -43,7 +33,3 @@ func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins
func (l *Loader) Unload(ctx context.Context, pluginID string) error { func (l *Loader) Unload(ctx context.Context, pluginID string) error {
return l.loader.Unload(ctx, pluginID) return l.loader.Unload(ctx, pluginID)
} }
func (l *Loader) PluginErrors() []*plugins.Error {
return l.loader.PluginErrors()
}

View File

@ -19,10 +19,6 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination"
"github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature"
@ -30,6 +26,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -63,7 +60,7 @@ func TestLoader_Load(t *testing.T) {
cfg *config.Cfg cfg *config.Cfg
pluginPaths []string pluginPaths []string
want []*plugins.Plugin want []*plugins.Plugin
pluginErrors map[string]*plugins.Error pluginErrors map[string]*plugins.SignatureError
}{ }{
{ {
name: "Load a Core plugin", name: "Load a Core plugin",
@ -275,10 +272,10 @@ func TestLoader_Load(t *testing.T) {
cfg: &config.Cfg{}, cfg: &config.Cfg{},
pluginPaths: []string{filepath.Join(testDataDir(t), "unsigned-datasource")}, pluginPaths: []string{filepath.Join(testDataDir(t), "unsigned-datasource")},
want: []*plugins.Plugin{}, want: []*plugins.Plugin{},
pluginErrors: map[string]*plugins.Error{ pluginErrors: map[string]*plugins.SignatureError{
"test-datasource": { "test-datasource": {
PluginID: "test-datasource", PluginID: "test-datasource",
ErrorCode: "signatureMissing", SignatureStatus: plugins.SignatureStatusUnsigned,
}, },
}, },
}, },
@ -327,10 +324,10 @@ func TestLoader_Load(t *testing.T) {
cfg: &config.Cfg{}, cfg: &config.Cfg{},
pluginPaths: []string{filepath.Join(testDataDir(t), "lacking-files")}, pluginPaths: []string{filepath.Join(testDataDir(t), "lacking-files")},
want: []*plugins.Plugin{}, want: []*plugins.Plugin{},
pluginErrors: map[string]*plugins.Error{ pluginErrors: map[string]*plugins.SignatureError{
"test-datasource": { "test-datasource": {
PluginID: "test-datasource", PluginID: "test-datasource",
ErrorCode: "signatureInvalid", SignatureStatus: plugins.SignatureStatusInvalid,
}, },
}, },
}, },
@ -342,10 +339,10 @@ func TestLoader_Load(t *testing.T) {
}, },
pluginPaths: []string{filepath.Join(testDataDir(t), "lacking-files")}, pluginPaths: []string{filepath.Join(testDataDir(t), "lacking-files")},
want: []*plugins.Plugin{}, want: []*plugins.Plugin{},
pluginErrors: map[string]*plugins.Error{ pluginErrors: map[string]*plugins.SignatureError{
"test-datasource": { "test-datasource": {
PluginID: "test-datasource", PluginID: "test-datasource",
ErrorCode: "signatureInvalid", SignatureStatus: plugins.SignatureStatusInvalid,
}, },
}, },
}, },
@ -357,10 +354,10 @@ func TestLoader_Load(t *testing.T) {
}, },
pluginPaths: []string{filepath.Join(testDataDir(t), "invalid-v2-missing-file")}, pluginPaths: []string{filepath.Join(testDataDir(t), "invalid-v2-missing-file")},
want: []*plugins.Plugin{}, want: []*plugins.Plugin{},
pluginErrors: map[string]*plugins.Error{ pluginErrors: map[string]*plugins.SignatureError{
"test-datasource": { "test-datasource": {
PluginID: "test-datasource", PluginID: "test-datasource",
ErrorCode: "signatureModified", SignatureStatus: plugins.SignatureStatusModified,
}, },
}, },
}, },
@ -372,10 +369,10 @@ func TestLoader_Load(t *testing.T) {
}, },
pluginPaths: []string{filepath.Join(testDataDir(t), "invalid-v2-extra-file")}, pluginPaths: []string{filepath.Join(testDataDir(t), "invalid-v2-extra-file")},
want: []*plugins.Plugin{}, want: []*plugins.Plugin{},
pluginErrors: map[string]*plugins.Error{ pluginErrors: map[string]*plugins.SignatureError{
"test-datasource": { "test-datasource": {
PluginID: "test-datasource", PluginID: "test-datasource",
ErrorCode: "signatureModified", SignatureStatus: plugins.SignatureStatusModified,
}, },
}, },
}, },
@ -433,7 +430,8 @@ func TestLoader_Load(t *testing.T) {
reg := fakes.NewFakePluginRegistry() reg := fakes.NewFakePluginRegistry()
procPrvdr := fakes.NewFakeBackendProcessProvider() procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr) errTracker := pluginerrs.ProvideSignatureErrorTracker()
l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr, errTracker)
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := l.Load(context.Background(), sources.NewLocalSource(tt.class, tt.pluginPaths)) got, err := l.Load(context.Background(), sources.NewLocalSource(tt.class, tt.pluginPaths))
@ -442,7 +440,7 @@ func TestLoader_Load(t *testing.T) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...)) t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...))
} }
pluginErrs := l.PluginErrors() pluginErrs := errTracker.SignatureErrors(context.Background())
require.Equal(t, len(tt.pluginErrors), len(pluginErrs)) require.Equal(t, len(tt.pluginErrors), len(pluginErrs))
for _, pluginErr := range pluginErrs { for _, pluginErr := range pluginErrs {
require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr) require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr)
@ -506,7 +504,7 @@ func TestLoader_Load_CustomSource(t *testing.T) {
Module: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module", Module: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module",
}} }}
l := newLoader(t, cfg, fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), fakes.NewFakeBackendProcessProvider()) l := newLoader(t, cfg, fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), fakes.NewFakeBackendProcessProvider(), newFakeSignatureErrorTracker())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{ got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class { PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.ClassBundled return plugins.ClassBundled
@ -536,7 +534,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
pluginPaths []string pluginPaths []string
existingPlugins map[string]struct{} existingPlugins map[string]struct{}
want []*plugins.Plugin want []*plugins.Plugin
pluginErrors map[string]*plugins.Error pluginErrors map[string]*plugins.SignatureError
}{ }{
{ {
name: "Load multiple plugins (broken, valid, unsigned)", name: "Load multiple plugins (broken, valid, unsigned)",
@ -583,10 +581,10 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
SignatureOrg: "Will Browne", SignatureOrg: "Will Browne",
}, },
}, },
pluginErrors: map[string]*plugins.Error{ pluginErrors: map[string]*plugins.SignatureError{
"test-panel": { "test-panel": {
PluginID: "test-panel", PluginID: "test-panel",
ErrorCode: "signatureMissing", SignatureStatus: plugins.SignatureStatusUnsigned,
}, },
}, },
}, },
@ -596,7 +594,9 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
reg := fakes.NewFakePluginRegistry() reg := fakes.NewFakePluginRegistry()
procPrvdr := fakes.NewFakeBackendProcessProvider() procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr) errTracker := pluginerrs.ProvideSignatureErrorTracker()
l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr, errTracker)
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := l.Load(context.Background(), &fakes.FakePluginSource{ got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class { PluginClassFunc: func(ctx context.Context) plugins.Class {
@ -613,7 +613,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
if !cmp.Equal(got, tt.want, compareOpts...) { if !cmp.Equal(got, tt.want, compareOpts...) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...)) t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...))
} }
pluginErrs := l.PluginErrors() pluginErrs := errTracker.SignatureErrors(context.Background())
require.Equal(t, len(tt.pluginErrors), len(pluginErrs)) require.Equal(t, len(tt.pluginErrors), len(pluginErrs))
for _, pluginErr := range pluginErrs { for _, pluginErr := range pluginErrs {
require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr) require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr)
@ -696,7 +696,7 @@ func TestLoader_Load_RBACReady(t *testing.T) {
reg := fakes.NewFakePluginRegistry() reg := fakes.NewFakePluginRegistry()
procPrvdr := fakes.NewFakeBackendProcessProvider() procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr) l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{ got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class { PluginClassFunc: func(ctx context.Context) plugins.Class {
@ -711,8 +711,6 @@ func TestLoader_Load_RBACReady(t *testing.T) {
if !cmp.Equal(got, tt.want, compareOpts...) { if !cmp.Equal(got, tt.want, compareOpts...) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...)) t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...))
} }
pluginErrs := l.PluginErrors()
require.Len(t, pluginErrs, 0)
verifyState(t, tt.want, reg, procPrvdr, procMgr) verifyState(t, tt.want, reg, procPrvdr, procMgr)
} }
@ -756,7 +754,7 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) {
procPrvdr := fakes.NewFakeBackendProcessProvider() procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
cfg := &config.Cfg{GrafanaAppURL: defaultAppURL} cfg := &config.Cfg{GrafanaAppURL: defaultAppURL}
l := newLoader(t, cfg, reg, procMgr, procPrvdr) l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{ got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class { PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.ClassExternal return plugins.ClassExternal
@ -832,7 +830,7 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
procPrvdr := fakes.NewFakeBackendProcessProvider() procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
cfg := &config.Cfg{} cfg := &config.Cfg{}
l := newLoader(t, cfg, reg, procMgr, procPrvdr) l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{ got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class { PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.ClassExternal return plugins.ClassExternal
@ -921,7 +919,7 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
} }
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
cfg := &config.Cfg{} cfg := &config.Cfg{}
l := newLoader(t, cfg, reg, procMgr, procPrvdr) l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{ got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class { PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.ClassExternal return plugins.ClassExternal
@ -1114,7 +1112,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
reg := fakes.NewFakePluginRegistry() reg := fakes.NewFakePluginRegistry()
cfg := &config.Cfg{} cfg := &config.Cfg{}
l := newLoader(t, cfg, reg, procMgr, procPrvdr) l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{ got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class { PluginClassFunc: func(ctx context.Context) plugins.Class {
@ -1289,7 +1287,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
procPrvdr := fakes.NewFakeBackendProcessProvider() procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
cfg := &config.Cfg{} cfg := &config.Cfg{}
l := newLoader(t, cfg, reg, procMgr, procPrvdr) l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{ got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class { PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.ClassExternal return plugins.ClassExternal
@ -1314,34 +1312,37 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
} }
func newLoader(t *testing.T, cfg *config.Cfg, reg registry.Service, proc process.Service, func newLoader(t *testing.T, cfg *config.Cfg, reg registry.Service, proc process.Service,
backendFactory plugins.BackendFactoryProvider) *Loader { backendFactory plugins.BackendFactoryProvider, sigErrTracker pluginerrs.SignatureErrorTracker) *Loader {
assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg)) assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg))
lic := fakes.NewFakeLicensingService() lic := fakes.NewFakeLicensingService()
angularInspector, err := angularinspector.NewStaticInspector() angularInspector := angularinspector.NewStaticInspector()
require.NoError(t, err)
terminate, err := pipeline.ProvideTerminationStage(cfg, reg, proc) terminate, err := pipeline.ProvideTerminationStage(cfg, reg, proc)
require.NoError(t, err) require.NoError(t, err)
return ProvideService(cfg, signature.NewUnsignedAuthorizer(cfg), proc, reg, fakes.NewFakeRoleRegistry(), assets, return ProvideService(pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(false), reg),
angularInspector, &fakes.FakeOauthService{},
pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(false), reg),
pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets), pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets),
pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector, sigErrTracker),
pipeline.ProvideInitializationStage(cfg, reg, lic, backendFactory, proc, &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry()), pipeline.ProvideInitializationStage(cfg, reg, lic, backendFactory, proc, &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry()),
terminate) terminate)
} }
func newLoaderWithAngularInspector(t *testing.T, cfg *config.Cfg, angularInspector angularinspector.Inspector) *Loader { func newLoaderWithAngularInspector(t *testing.T, cfg *config.Cfg, angularInspector angularinspector.Inspector) *Loader {
assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg))
lic := fakes.NewFakeLicensingService()
reg := fakes.NewFakePluginRegistry() reg := fakes.NewFakePluginRegistry()
backendFactory := fakes.NewFakeBackendProcessProvider()
proc := fakes.NewFakeProcessManager()
terminationStage, err := termination.New(cfg, termination.Opts{}) terminate, err := pipeline.ProvideTerminationStage(cfg, reg, proc)
require.NoError(t, err) require.NoError(t, err)
sigErrTracker := pluginerrs.ProvideSignatureErrorTracker()
return ProvideService(cfg, signature.NewUnsignedAuthorizer(cfg), process.ProvideService(reg), reg, return ProvideService(pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(false), reg),
fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets),
angularInspector, &fakes.FakeOauthService{}, pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector, sigErrTracker),
discovery.New(cfg, discovery.Opts{}), bootstrap.New(cfg, bootstrap.Opts{}), pipeline.ProvideInitializationStage(cfg, reg, lic, backendFactory, proc, &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry()),
initialization.New(cfg, initialization.Opts{}), terminationStage) terminate)
} }
func verifyState(t *testing.T, ps []*plugins.Plugin, reg registry.Service, func verifyState(t *testing.T, ps []*plugins.Plugin, reg registry.Service,

View File

@ -6,15 +6,19 @@ import (
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/envvars" "github.com/grafana/grafana/pkg/plugins/envvars"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation"
"github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/oauth" "github.com/grafana/grafana/pkg/plugins/oauth"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
) )
func ProvideDiscoveryStage(cfg *config.Cfg, pf finder.Finder, pr registry.Service) *discovery.Discovery { func ProvideDiscoveryStage(cfg *config.Cfg, pf finder.Finder, pr registry.Service) *discovery.Discovery {
@ -37,6 +41,17 @@ func ProvideBootstrapStage(cfg *config.Cfg, sc plugins.SignatureCalculator, a *a
}) })
} }
func ProvideValidationStage(cfg *config.Cfg, sv signature.Validator, ai angularinspector.Inspector,
et pluginerrs.SignatureErrorTracker) *validation.Validate {
return validation.New(cfg, validation.Opts{
ValidateFuncs: []validation.ValidateFunc{
SignatureValidationStep(sv, et),
validation.ModuleJSValidationStep(),
validation.AngularDetectionStep(cfg, ai),
},
})
}
func ProvideInitializationStage(cfg *config.Cfg, pr registry.Service, l plugins.Licensing, func ProvideInitializationStage(cfg *config.Cfg, pr registry.Service, l plugins.Licensing,
bp plugins.BackendFactoryProvider, pm process.Service, externalServiceRegistry oauth.ExternalServiceRegistry, bp plugins.BackendFactoryProvider, pm process.Service, externalServiceRegistry oauth.ExternalServiceRegistry,
roleRegistry plugins.RoleRegistry) *initialization.Initialize { roleRegistry plugins.RoleRegistry) *initialization.Initialize {

View File

@ -2,14 +2,18 @@ package pipeline
import ( import (
"context" "context"
"errors"
"github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/oauth" "github.com/grafana/grafana/pkg/plugins/oauth"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
) )
// ExternalServiceRegistration implements an InitializeFunc for registering external services. // ExternalServiceRegistration implements an InitializeFunc for registering external services.
@ -78,3 +82,42 @@ func ReportBuildMetrics(_ context.Context, p *plugins.Plugin) (*plugins.Plugin,
} }
return p, nil return p, nil
} }
// SignatureValidation implements a ValidateFunc for validating plugin signatures.
type SignatureValidation struct {
signatureValidator signature.Validator
errs pluginerrs.SignatureErrorTracker
log log.Logger
}
// SignatureValidationStep returns a new ValidateFunc for validating plugin signatures.
func SignatureValidationStep(signatureValidator signature.Validator,
sigErr pluginerrs.SignatureErrorTracker) validation.ValidateFunc {
sv := &SignatureValidation{
errs: sigErr,
signatureValidator: signatureValidator,
log: log.New("plugins.signature.validation"),
}
return sv.Validate
}
// Validate validates the plugin signature. If a signature error is encountered, the error is recorded with the
// pluginerrs.SignatureErrorTracker.
func (v *SignatureValidation) Validate(ctx context.Context, p *plugins.Plugin) error {
err := v.signatureValidator.ValidateSignature(p)
if err != nil {
var sigErr *plugins.SignatureError
if errors.As(err, &sigErr) {
v.log.Warn("Skipping loading plugin due to problem with signature",
"pluginID", p.ID, "status", sigErr.SignatureStatus)
p.SignatureError = sigErr
v.errs.Record(ctx, sigErr)
}
return err
}
// clear plugin error if a pre-existing error has since been resolved
v.errs.Clear(ctx, p.ID)
return nil
}

View File

@ -0,0 +1,71 @@
package pluginerrs
import (
"context"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/log"
)
var _ plugins.ErrorResolver = (*Store)(nil)
type Store struct {
signatureErrs SignatureErrorTracker
}
func ProvideStore(signatureErrs SignatureErrorTracker) *Store {
return &Store{
signatureErrs: signatureErrs,
}
}
func (s *Store) PluginErrors() []*plugins.Error {
sigErrs := s.signatureErrs.SignatureErrors(context.Background())
errs := make([]*plugins.Error, 0, len(sigErrs))
for _, err := range sigErrs {
errs = append(errs, &plugins.Error{
PluginID: err.PluginID,
ErrorCode: err.AsErrorCode(),
})
}
return errs
}
type SignatureErrorRegistry struct {
errs map[string]*plugins.SignatureError
log log.Logger
}
type SignatureErrorTracker interface {
Record(ctx context.Context, err *plugins.SignatureError)
Clear(ctx context.Context, pluginID string)
SignatureErrors(ctx context.Context) []*plugins.SignatureError
}
func ProvideSignatureErrorTracker() *SignatureErrorRegistry {
return newSignatureErrorRegistry()
}
func newSignatureErrorRegistry() *SignatureErrorRegistry {
return &SignatureErrorRegistry{
errs: make(map[string]*plugins.SignatureError),
log: log.New("plugins.errors"),
}
}
func (r *SignatureErrorRegistry) Record(_ context.Context, signatureErr *plugins.SignatureError) {
r.errs[signatureErr.PluginID] = signatureErr
}
func (r *SignatureErrorRegistry) Clear(_ context.Context, pluginID string) {
delete(r.errs, pluginID)
}
func (r *SignatureErrorRegistry) SignatureErrors(_ context.Context) []*plugins.SignatureError {
errs := make([]*plugins.SignatureError, 0, len(r.errs))
for _, err := range r.errs {
errs = append(errs, err)
}
return errs
}

View File

@ -1,4 +1,4 @@
package manager package pluginsintegration
import ( import (
"context" "context"
@ -17,26 +17,10 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/manager/client"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
"github.com/grafana/grafana/pkg/plugins/manager/sources"
"github.com/grafana/grafana/pkg/plugins/manager/store" "github.com/grafana/grafana/pkg/plugins/manager/store"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/pluginsintegration/config"
plicensing "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
"github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor" "github.com/grafana/grafana/pkg/tsdb/azuremonitor"
@ -75,7 +59,7 @@ func TestIntegrationPluginManager(t *testing.T) {
app_mode = production app_mode = production
[plugin.test-app] [plugin.test-app]
path=testdata/test-app path=../../plugins/manager/testdata/test-app
[plugin.test-panel] [plugin.test-panel]
not=included not=included
@ -113,37 +97,16 @@ func TestIntegrationPluginManager(t *testing.T) {
graf := grafanads.ProvideService(sv2, nil) graf := grafanads.ProvideService(sv2, nil)
phlare := pyroscope.ProvideService(hcp, acimpl.ProvideAccessControl(cfg)) phlare := pyroscope.ProvideService(hcp, acimpl.ProvideAccessControl(cfg))
parca := parca.ProvideService(hcp) parca := parca.ProvideService(hcp)
coreRegistry := coreplugin.ProvideCoreRegistry(am, cw, cm, es, grap, idb, lk, otsdb, pr, tmpo, td, pg, my, ms, graf, phlare, parca) coreRegistry := coreplugin.ProvideCoreRegistry(am, cw, cm, es, grap, idb, lk, otsdb, pr, tmpo, td, pg, my, ms, graf, phlare, parca)
pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures()) testCtx := CreateIntegrationTestCtx(t, cfg, coreRegistry)
require.NoError(t, err)
reg := registry.ProvideService()
lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg})
angularInspector, err := angularinspector.NewStaticInspector()
require.NoError(t, err)
proc := process.NewManager(reg)
discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg)
bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)))
initialize := pipeline.ProvideInitializationStage(pCfg, reg, lic, provider.ProvideService(coreRegistry), proc, &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry())
terminate, err := pipeline.ProvideTerminationStage(pCfg, reg, proc)
require.NoError(t, err)
l := loader.ProvideService(pCfg, signature.NewUnsignedAuthorizer(pCfg),
reg, fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)),
angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize, terminate)
srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l)
require.NoError(t, err)
ctx := context.Background() ctx := context.Background()
verifyCorePluginCatalogue(t, ctx, ps) verifyCorePluginCatalogue(t, ctx, testCtx.PluginStore)
verifyBundledPlugins(t, ctx, ps) verifyBundledPlugins(t, ctx, testCtx.PluginStore)
verifyPluginStaticRoutes(t, ctx, ps, reg) verifyPluginStaticRoutes(t, ctx, testCtx.PluginStore, testCtx.PluginRegistry)
verifyBackendProcesses(t, reg.Plugins(ctx)) verifyBackendProcesses(t, testCtx.PluginRegistry.Plugins(ctx))
verifyPluginQuery(t, ctx, client.ProvideService(reg, pCfg)) verifyPluginQuery(t, ctx, testCtx.PluginClient)
} }
func verifyPluginQuery(t *testing.T, ctx context.Context, c plugins.Client) { func verifyPluginQuery(t *testing.T, ctx context.Context, c plugins.Client) {

View File

@ -19,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation"
"github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature"
@ -42,6 +43,7 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsintegration/loader" "github.com/grafana/grafana/pkg/services/pluginsintegration/loader"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
"github.com/grafana/grafana/pkg/services/pluginsintegration/serviceregistration" "github.com/grafana/grafana/pkg/services/pluginsintegration/serviceregistration"
@ -72,15 +74,22 @@ var WireSet = wire.NewSet(
wire.Bind(new(initialization.Initializer), new(*initialization.Initialize)), wire.Bind(new(initialization.Initializer), new(*initialization.Initialize)),
pipeline.ProvideTerminationStage, pipeline.ProvideTerminationStage,
wire.Bind(new(termination.Terminator), new(*termination.Terminate)), wire.Bind(new(termination.Terminator), new(*termination.Terminate)),
pipeline.ProvideValidationStage,
wire.Bind(new(validation.Validator), new(*validation.Validate)),
angularpatternsstore.ProvideService, angularpatternsstore.ProvideService,
angulardetectorsprovider.ProvideDynamic, angulardetectorsprovider.ProvideDynamic,
angularinspector.ProvideService, angularinspector.ProvideService,
wire.Bind(new(pAngularInspector.Inspector), new(*angularinspector.Service)), wire.Bind(new(pAngularInspector.Inspector), new(*angularinspector.Service)),
signature.ProvideValidatorService,
wire.Bind(new(signature.Validator), new(*signature.Validation)),
loader.ProvideService, loader.ProvideService,
wire.Bind(new(pluginLoader.Service), new(*loader.Loader)), wire.Bind(new(pluginLoader.Service), new(*loader.Loader)),
wire.Bind(new(plugins.ErrorResolver), new(*loader.Loader)), pluginerrs.ProvideSignatureErrorTracker,
wire.Bind(new(pluginerrs.SignatureErrorTracker), new(*pluginerrs.SignatureErrorRegistry)),
pluginerrs.ProvideStore,
wire.Bind(new(plugins.ErrorResolver), new(*pluginerrs.Store)),
manager.ProvideInstaller, manager.ProvideInstaller,
wire.Bind(new(plugins.Installer), new(*manager.PluginInstaller)), wire.Bind(new(plugins.Installer), new(*manager.PluginInstaller)),
registry.ProvideService, registry.ProvideService,

View File

@ -0,0 +1,114 @@
package pluginsintegration
import (
"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/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
pluginsCfg "github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/client"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation"
"github.com/grafana/grafana/pkg/plugins/manager/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
"github.com/grafana/grafana/pkg/plugins/manager/sources"
"github.com/grafana/grafana/pkg/plugins/manager/store"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/config"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
"github.com/grafana/grafana/pkg/setting"
)
type IntegrationTestCtx struct {
PluginClient plugins.Client
PluginStore *store.Service
PluginRegistry registry.Service
}
func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *coreplugin.Registry) *IntegrationTestCtx {
pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures())
require.NoError(t, err)
cdn := pluginscdn.ProvideService(pCfg)
reg := registry.ProvideService()
angularInspector := angularinspector.NewStaticInspector()
proc := process.NewManager(reg)
errTracker := pluginerrs.ProvideSignatureErrorTracker()
disc := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(true), reg)
boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(cdn))
valid := pipeline.ProvideValidationStage(pCfg, signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg)), angularInspector, errTracker)
init := pipeline.ProvideInitializationStage(pCfg, reg, fakes.NewFakeLicensingService(), provider.ProvideService(coreRegistry), proc, &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry())
term, err := pipeline.ProvideTerminationStage(pCfg, reg, proc)
require.NoError(t, err)
l := CreateTestLoader(t, pCfg, LoaderOpts{
Discoverer: disc,
Bootstrapper: boot,
Validator: valid,
Initializer: init,
Terminator: term,
})
ps, err := store.ProvideService(reg, sources.ProvideService(cfg, pCfg), l)
require.NoError(t, err)
return &IntegrationTestCtx{
PluginClient: client.ProvideService(reg, pCfg),
PluginStore: ps,
PluginRegistry: reg,
}
}
type LoaderOpts struct {
Discoverer discovery.Discoverer
Bootstrapper bootstrap.Bootstrapper
Validator validation.Validator
Terminator termination.Terminator
Initializer initialization.Initializer
}
func CreateTestLoader(t *testing.T, cfg *pluginsCfg.Cfg, opts LoaderOpts) *loader.Loader {
if opts.Discoverer == nil {
opts.Discoverer = pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(cfg.DevMode), registry.ProvideService())
}
if opts.Bootstrapper == nil {
opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(cfg)))
}
if opts.Validator == nil {
opts.Validator = pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularinspector.NewStaticInspector(), pluginerrs.ProvideSignatureErrorTracker())
}
if opts.Initializer == nil {
reg := registry.ProvideService()
coreRegistry := coreplugin.NewRegistry(make(map[string]backendplugin.PluginFactoryFunc))
opts.Initializer = pipeline.ProvideInitializationStage(cfg, reg, fakes.NewFakeLicensingService(), provider.ProvideService(coreRegistry), process.NewManager(reg), &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry())
}
if opts.Terminator == nil {
var err error
reg := registry.ProvideService()
opts.Terminator, err = pipeline.ProvideTerminationStage(cfg, reg, process.NewManager(reg))
require.NoError(t, err)
}
return loader.New(opts.Discoverer, opts.Bootstrapper, opts.Validator, opts.Initializer, opts.Terminator)
}