Plugins: Angular detector: Remote patterns fetching (#69843)

* Plugins: Angular detector: Remote patterns fetching

* Renamed PatternType to GCOMPatternType

* Renamed files

* Renamed more files

* Moved files again

* Add type checks, unexport GCOM structs

* Cache failures, update log messages, fix GCOM URL

* Fail silently for unknown pattern types, update docstrings

* Fix tests

* Rename gcomPattern.Value to gcomPattern.Pattern

* Refactoring

* Add FlagPluginsRemoteAngularDetectionPatterns feature flag

* Fix tests

* Re-generate feature flags

* Add TestProvideInspector, renamed TestDefaultStaticDetectorsInspector

* Add TestProvideInspector

* Add TestContainsBytesDetector and TestRegexDetector

* Renamed getter to provider

* More tests

* TestStaticDetectorsProvider, TestSequenceDetectorsProvider

* GCOM tests

* Lint

* Made detector.detect unexported, updated docstrings

* Allow changing grafana.com URL

* Fix API path, add more logs

* Update tryUpdateRemoteDetectors docstring

* Use angulardetector http client

* Return false, nil if module.js does not exist

* Chore: Split angualrdetector into angularinspector and angulardetector packages

Moved files around, changed references and fixed tests:
- Split the old angulardetector package into angular/angulardetector and angular/angularinspector
- angulardetector provides the detection structs/interfaces (Detector, DetectorsProvider...)
- angularinspector provides the actual angular detection service used directly in pluginsintegration
- Exported most of the stuff that was private and now put into angulardetector, as it is not required by angularinspector

* Renamed detector.go -> angulardetector.go and inspector.go -> angularinspector.go

Forgot to rename those two files to match the package's names

* Renamed angularinspector.ProvideInspector to angularinspector.ProvideService

* Renamed "harcoded" to "static" and "remote" to "dynamic"

from PR review, matches the same naming schema used for signing keys fetching

* Fix merge conflict on updated angular patterns

* Removed GCOM cache

* Renamed Detect to DetectAngular and Detector to AngularDetector

* Fix call to NewGCOMDetectorsProvider in newDynamicInspector

* Removed unused test function newError500GCOMScenario

* Added angularinspector service definition in pluginsintegration

* Moved dynamic inspector into pluginsintegration

* Move gcom angulardetectorsprovider into pluginsintegration

* Log errUnknownPatternType at debug level

* re-generate feature flags

* fix error log
pull/69764/head^2
Giuseppe Guerra 3 years ago committed by GitHub
parent 903af7e29c
commit cca9d89733
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  2. 1
      packages/grafana-data/src/types/featureToggles.gen.ts
  3. 6
      pkg/api/plugin_resource_test.go
  4. 5
      pkg/plugins/config/config.go
  5. 69
      pkg/plugins/manager/loader/angular/angulardetector/angulardetector.go
  6. 120
      pkg/plugins/manager/loader/angular/angulardetector/angulardetector_test.go
  7. 78
      pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go
  8. 145
      pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go
  9. 18
      pkg/plugins/manager/loader/angular/angularinspector/fakes.go
  10. 35
      pkg/plugins/manager/loader/angular/angularinspector/fakes_test.go
  11. 46
      pkg/plugins/manager/loader/angulardetector/angulardetector.go
  12. 95
      pkg/plugins/manager/loader/angulardetector/angulardetector_test.go
  13. 60
      pkg/plugins/manager/loader/angulardetector/service.go
  14. 17
      pkg/plugins/manager/loader/loader.go
  15. 34
      pkg/plugins/manager/loader/loader_test.go
  16. 6
      pkg/plugins/manager/manager_integration_test.go
  17. 7
      pkg/services/featuremgmt/registry.go
  18. 1
      pkg/services/featuremgmt/toggles_gen.csv
  19. 4
      pkg/services/featuremgmt/toggles_gen.go
  20. 157
      pkg/services/pluginsintegration/angulardetector/gcom.go
  21. 144
      pkg/services/pluginsintegration/angulardetector/gcom_test.go
  22. 45
      pkg/services/pluginsintegration/angularinspector/angularinspector.go
  23. 42
      pkg/services/pluginsintegration/angularinspector/angularinspector_test.go
  24. 1
      pkg/services/pluginsintegration/config/config.go
  25. 6
      pkg/services/pluginsintegration/pluginsintegration.go

@ -73,7 +73,7 @@ These features are early in their development lifecycle and so are not yet suppo
Experimental features might be changed or removed without prior notice. Experimental features might be changed or removed without prior notice.
| Feature toggle name | Description | | Feature toggle name | Description |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------ | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | | `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread |
| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | | `queryOverLive` | Use Grafana Live WebSocket to execute backend queries |
| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | | `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) |
@ -114,6 +114,7 @@ Experimental features might be changed or removed without prior notice.
| `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries | | `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries |
| `exploreScrollableLogsContainer` | Improves the scrolling behavior of logs in Explore | | `exploreScrollableLogsContainer` | Improves the scrolling behavior of logs in Explore |
| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | | `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries |
| `pluginsDynamicAngularDetectionPatterns` | Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones |
| `alertingLokiRangeToInstant` | Rewrites eligible loki range queries to instant queries | | `alertingLokiRangeToInstant` | Rewrites eligible loki range queries to instant queries |
| `flameGraphV2` | New version of flame graph with new features | | `flameGraphV2` | New version of flame graph with new features |

@ -101,6 +101,7 @@ export interface FeatureToggles {
cloudWatchLogsMonacoEditor?: boolean; cloudWatchLogsMonacoEditor?: boolean;
exploreScrollableLogsContainer?: boolean; exploreScrollableLogsContainer?: boolean;
recordedQueriesMulti?: boolean; recordedQueriesMulti?: boolean;
pluginsDynamicAngularDetectionPatterns?: boolean;
alertingLokiRangeToInstant?: boolean; alertingLokiRangeToInstant?: boolean;
flameGraphV2?: boolean; flameGraphV2?: boolean;
} }

@ -11,6 +11,7 @@ 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"
@ -22,7 +23,6 @@ import (
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"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angulardetector"
"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/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
@ -67,10 +67,12 @@ func TestCallResource(t *testing.T) {
pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures()) pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures())
require.NoError(t, err) require.NoError(t, err)
reg := registry.ProvideService() reg := registry.ProvideService()
angularInspector, err := angularinspector.NewStaticInspector()
require.NoError(t, err)
l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg), l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg),
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(), reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, statickey.New()),
angulardetector.NewDefaultPatternsListInspector()) angularInspector)
srcs := sources.ProvideService(cfg, pCfg) srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l) ps, err := store.ProvideService(reg, srcs, l)
require.NoError(t, err) require.NoError(t, err)

@ -45,7 +45,8 @@ type Cfg struct {
func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string, func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string,
awsAllowedAuthProviders []string, awsAssumeRoleEnabled bool, azure *azsettings.AzureSettings, secureSocksDSProxy setting.SecureSocksDSProxySettings, awsAllowedAuthProviders []string, awsAssumeRoleEnabled bool, azure *azsettings.AzureSettings, secureSocksDSProxy setting.SecureSocksDSProxySettings,
grafanaVersion string, logDatasourceRequests bool, pluginsCDNURLTemplate string, tracing Tracing, features plugins.FeatureToggles, angularSupportEnabled bool) *Cfg { grafanaVersion string, logDatasourceRequests bool, pluginsCDNURLTemplate string, tracing Tracing, features plugins.FeatureToggles, angularSupportEnabled bool,
grafanaComURL string) *Cfg {
return &Cfg{ return &Cfg{
log: log.New("plugin.cfg"), log: log.New("plugin.cfg"),
PluginsPath: pluginsPath, PluginsPath: pluginsPath,
@ -60,7 +61,7 @@ func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSetti
LogDatasourceRequests: logDatasourceRequests, LogDatasourceRequests: logDatasourceRequests,
PluginsCDNURLTemplate: pluginsCDNURLTemplate, PluginsCDNURLTemplate: pluginsCDNURLTemplate,
Tracing: tracing, Tracing: tracing,
GrafanaComURL: "https://grafana.com", GrafanaComURL: grafanaComURL,
Features: features, Features: features,
AngularSupportEnabled: angularSupportEnabled, AngularSupportEnabled: angularSupportEnabled,
} }

@ -0,0 +1,69 @@
package angulardetector
import (
"bytes"
"context"
"regexp"
)
var (
_ AngularDetector = &ContainsBytesDetector{}
_ AngularDetector = &RegexDetector{}
_ DetectorsProvider = &StaticDetectorsProvider{}
_ DetectorsProvider = SequenceDetectorsProvider{}
)
// AngularDetector implements a check to see if a js file is using angular APIs.
type AngularDetector interface {
// DetectAngular takes the content of a js file and returns true if the plugin is using Angular.
DetectAngular(js []byte) bool
}
// ContainsBytesDetector is an AngularDetector that returns true if module.js contains the "pattern" string.
type ContainsBytesDetector struct {
Pattern []byte
}
// DetectAngular returns true if moduleJs contains the byte slice d.pattern.
func (d *ContainsBytesDetector) DetectAngular(moduleJs []byte) bool {
return bytes.Contains(moduleJs, d.Pattern)
}
// RegexDetector is an AngularDetector that returns true if the module.js content matches a regular expression.
type RegexDetector struct {
Regex *regexp.Regexp
}
// DetectAngular returns true if moduleJs matches the regular expression d.regex.
func (d *RegexDetector) DetectAngular(moduleJs []byte) bool {
return d.Regex.Match(moduleJs)
}
// DetectorsProvider can provide multiple AngularDetectors used for Angular detection.
type DetectorsProvider interface {
// ProvideDetectors returns a slice of AngularDetector.
ProvideDetectors(ctx context.Context) []AngularDetector
}
// StaticDetectorsProvider is a DetectorsProvider that always returns a pre-defined slice of AngularDetector.
type StaticDetectorsProvider struct {
Detectors []AngularDetector
}
func (p *StaticDetectorsProvider) ProvideDetectors(_ context.Context) []AngularDetector {
return p.Detectors
}
// SequenceDetectorsProvider is a DetectorsProvider that wraps a slice of other DetectorsProvider, and returns the first
// provided result that isn't empty.
type SequenceDetectorsProvider []DetectorsProvider
func (p SequenceDetectorsProvider) ProvideDetectors(ctx context.Context) []AngularDetector {
for _, provider := range p {
if detectors := provider.ProvideDetectors(ctx); len(detectors) > 0 {
return detectors
}
}
return nil
}

@ -0,0 +1,120 @@
package angulardetector
import (
"context"
"regexp"
"testing"
"github.com/stretchr/testify/require"
)
var testDetectors = []AngularDetector{
&ContainsBytesDetector{Pattern: []byte("PanelCtrl")},
&ContainsBytesDetector{Pattern: []byte("QueryCtrl")},
}
func TestContainsBytesDetector(t *testing.T) {
detector := &ContainsBytesDetector{Pattern: []byte("needle")}
t.Run("contains", func(t *testing.T) {
require.True(t, detector.DetectAngular([]byte("lorem needle ipsum haystack")))
})
t.Run("not contains", func(t *testing.T) {
require.False(t, detector.DetectAngular([]byte("ippif")))
})
}
func TestRegexDetector(t *testing.T) {
detector := &RegexDetector{Regex: regexp.MustCompile("hello world(?s)")}
for _, tc := range []struct {
name string
s string
exp bool
}{
{name: "match 1", s: "hello world", exp: true},
{name: "match 2", s: "bla bla hello world bla bla", exp: true},
{name: "match 3", s: "bla bla hello worlds bla bla", exp: true},
{name: "no match", s: "bla bla hello you reading this test code", exp: false},
} {
t.Run(tc.s, func(t *testing.T) {
r := detector.DetectAngular([]byte(tc.s))
require.Equal(t, tc.exp, r, "DetectAngular result should be correct")
})
}
}
func TestStaticDetectorsProvider(t *testing.T) {
p := StaticDetectorsProvider{Detectors: testDetectors}
detectors := p.ProvideDetectors(context.Background())
require.NotEmpty(t, detectors)
require.Equal(t, testDetectors, detectors)
}
type fakeDetectorsProvider struct {
calls int
returns []AngularDetector
}
func (p *fakeDetectorsProvider) ProvideDetectors(_ context.Context) []AngularDetector {
p.calls += 1
return p.returns
}
func TestSequenceDetectorsProvider(t *testing.T) {
for _, tc := range []struct {
name string
fakeProviders []*fakeDetectorsProvider
exp func(t *testing.T, fakeProviders []*fakeDetectorsProvider, detectors []AngularDetector)
}{
{
name: "returns first non-empty provided angularDetectors (first)",
fakeProviders: []*fakeDetectorsProvider{
{returns: testDetectors},
{returns: nil},
},
exp: func(t *testing.T, fakeProviders []*fakeDetectorsProvider, detectors []AngularDetector) {
require.NotEmpty(t, detectors)
require.Len(t, detectors, len(fakeProviders[0].returns))
require.Equal(t, fakeProviders[0].returns, detectors)
require.Equal(t, 1, fakeProviders[0].calls, "fake provider 0 should be called")
require.Zero(t, fakeProviders[1].calls, "fake provider 1 should not be called")
},
},
{
name: "returns first non-empty provided angularDetectors (second)",
fakeProviders: []*fakeDetectorsProvider{
{returns: nil},
{returns: testDetectors},
},
exp: func(t *testing.T, fakeProviders []*fakeDetectorsProvider, detectors []AngularDetector) {
require.NotEmpty(t, detectors)
require.Len(t, detectors, len(fakeProviders[1].returns))
require.Equal(t, fakeProviders[1].returns, detectors)
for i, p := range fakeProviders {
require.Equalf(t, 1, p.calls, "fake provider %d should be called", i)
}
},
},
{
name: "returns nil if all providers return empty",
fakeProviders: []*fakeDetectorsProvider{
{returns: nil},
{returns: []AngularDetector{}},
},
exp: func(t *testing.T, fakeProviders []*fakeDetectorsProvider, detectors []AngularDetector) {
require.Empty(t, detectors, "should not return any angularDetectors")
for i, p := range fakeProviders {
require.Equalf(t, 1, p.calls, "fake provider %d should be called", i)
}
},
},
} {
t.Run(tc.name, func(t *testing.T) {
seq := make(SequenceDetectorsProvider, 0, len(tc.fakeProviders))
for _, p := range tc.fakeProviders {
seq = append(seq, DetectorsProvider(p))
}
detectors := seq.ProvideDetectors(context.Background())
tc.exp(t, tc.fakeProviders, detectors)
})
}
}

@ -0,0 +1,78 @@
package angularinspector
import (
"context"
"errors"
"fmt"
"io"
"regexp"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector"
)
// Inspector can inspect a plugin and determine if it's an Angular plugin or not.
type Inspector interface {
// Inspect takes a plugin and checks if the plugin is using Angular.
Inspect(ctx context.Context, p *plugins.Plugin) (bool, error)
}
// PatternsListInspector is an Inspector that matches a plugin's module.js against all the patterns returned by
// the detectorsProvider, in sequence.
type PatternsListInspector struct {
// DetectorsProvider returns the detectors that will be used by Inspect.
DetectorsProvider angulardetector.DetectorsProvider
}
func (i *PatternsListInspector) Inspect(ctx context.Context, p *plugins.Plugin) (isAngular bool, err error) {
f, err := p.FS.Open("module.js")
if err != nil {
if errors.Is(err, plugins.ErrFileNotExist) {
// We may not have a module.js for some backend plugins, so ignore the error if module.js does not exist
return false, nil
}
return false, err
}
defer func() {
if closeErr := f.Close(); closeErr != nil && err == nil {
err = fmt.Errorf("close module.js: %w", closeErr)
}
}()
b, err := io.ReadAll(f)
if err != nil {
return false, fmt.Errorf("module.js readall: %w", err)
}
for _, d := range i.DetectorsProvider.ProvideDetectors(ctx) {
if d.DetectAngular(b) {
isAngular = true
break
}
}
return
}
// defaultDetectors contains all the detectors to DetectAngular Angular plugins.
// They are executed in the specified order.
var defaultDetectors = []angulardetector.AngularDetector{
&angulardetector.ContainsBytesDetector{Pattern: []byte("PanelCtrl")},
&angulardetector.ContainsBytesDetector{Pattern: []byte("ConfigCtrl")},
&angulardetector.ContainsBytesDetector{Pattern: []byte("app/plugins/sdk")},
&angulardetector.ContainsBytesDetector{Pattern: []byte("angular.isNumber(")},
&angulardetector.ContainsBytesDetector{Pattern: []byte("editor.html")},
&angulardetector.ContainsBytesDetector{Pattern: []byte("ctrl.annotation")},
&angulardetector.ContainsBytesDetector{Pattern: []byte("getLegacyAngularInjector")},
&angulardetector.RegexDetector{Regex: regexp.MustCompile(`["']QueryCtrl["']`)},
}
// NewDefaultStaticDetectorsProvider returns a new StaticDetectorsProvider with the default (static, hardcoded) angular
// detection patterns (defaultDetectors)
func NewDefaultStaticDetectorsProvider() angulardetector.DetectorsProvider {
return &angulardetector.StaticDetectorsProvider{Detectors: defaultDetectors}
}
// NewStaticInspector returns the default Inspector, which is a PatternsListInspector that only uses the
// static (hardcoded) angular detection patterns.
func NewStaticInspector() (Inspector, error) {
return &PatternsListInspector{DetectorsProvider: NewDefaultStaticDetectorsProvider()}, nil
}

@ -0,0 +1,145 @@
package angularinspector
import (
"context"
"strconv"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector"
)
type fakeDetector struct {
calls int
returns bool
}
func (d *fakeDetector) DetectAngular(_ []byte) bool {
d.calls += 1
return d.returns
}
func TestPatternsListInspector(t *testing.T) {
plugin := &plugins.Plugin{
FS: plugins.NewInMemoryFS(map[string][]byte{"module.js": nil}),
}
for _, tc := range []struct {
name string
fakeDetectors []*fakeDetector
exp func(t *testing.T, r bool, err error, fakeDetectors []*fakeDetector)
}{
{
name: "calls the detectors in sequence until true is returned",
fakeDetectors: []*fakeDetector{
{returns: false},
{returns: true},
{returns: false},
},
exp: func(t *testing.T, r bool, err error, fakeDetectors []*fakeDetector) {
require.NoError(t, err)
require.True(t, r, "inspector should return true")
require.Equal(t, 1, fakeDetectors[0].calls, "fake 0 should be called")
require.Equal(t, 1, fakeDetectors[1].calls, "fake 1 should be called")
require.Equal(t, 0, fakeDetectors[2].calls, "fake 2 should not be called")
},
},
{
name: "calls the detectors in sequence and returns false as default",
fakeDetectors: []*fakeDetector{
{returns: false},
{returns: false},
},
exp: func(t *testing.T, r bool, err error, fakeDetectors []*fakeDetector) {
require.NoError(t, err)
require.False(t, r, "inspector should return false")
require.Equal(t, 1, fakeDetectors[0].calls, "fake 0 should not be called")
require.Equal(t, 1, fakeDetectors[1].calls, "fake 1 should not be called")
},
},
{
name: "empty detectors should return false",
fakeDetectors: nil,
exp: func(t *testing.T, r bool, err error, fakeDetectors []*fakeDetector) {
require.NoError(t, err)
require.False(t, r, "inspector should return false")
},
},
} {
t.Run(tc.name, func(t *testing.T) {
detectors := make([]angulardetector.AngularDetector, 0, len(tc.fakeDetectors))
for _, d := range tc.fakeDetectors {
detectors = append(detectors, angulardetector.AngularDetector(d))
}
inspector := &PatternsListInspector{
DetectorsProvider: &angulardetector.StaticDetectorsProvider{Detectors: detectors},
}
r, err := inspector.Inspect(context.Background(), plugin)
tc.exp(t, r, err, tc.fakeDetectors)
})
}
}
func TestDefaultStaticDetectorsInspector(t *testing.T) {
// Tests the default hardcoded angular patterns
type tc struct {
name string
plugin *plugins.Plugin
exp bool
}
var tcs []tc
// Angular imports
for i, content := range [][]byte{
[]byte(`import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk';`),
[]byte(`define(["app/plugins/sdk"],(function(n){return function(n){var t={};function e(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return n[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=n,e.c=t,e.d=function(n,t,r){e.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:r})},e.r=function(n){"undefined"!=typeof`),
[]byte(`define(["app/plugins/sdk"],(function(n){return function(n){var t={};function e(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return n[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=n,e.c=t,e.d=function(n,t,r){e.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:r})},e.r=function(n){"undefined"!=typeof Symbol&&Symbol.toSt`),
[]byte(`define(["react","lodash","@grafana/data","@grafana/ui","@emotion/css","@grafana/runtime","moment","app/core/utils/datemath","jquery","app/plugins/sdk","app/core/core_module","app/core/core","app/core/table_model","app/core/utils/kbn","app/core/config","angular"],(function(e,t,r,n,i,a,o,s,u,l,c,p,f,h,d,m){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};retur`),
[]byte(`exports_1("QueryCtrl", query_ctrl_1.PluginQueryCtrl);`),
[]byte(`exports_1('QueryCtrl', query_ctrl_1.PluginQueryCtrl);`),
} {
tcs = append(tcs, tc{
name: "angular " + strconv.Itoa(i),
plugin: &plugins.Plugin{
FS: plugins.NewInMemoryFS(map[string][]byte{
"module.js": content,
}),
},
exp: true,
})
}
// Not angular (test against possible false detections)
for i, content := range [][]byte{
[]byte(`import { PanelPlugin } from '@grafana/data'`),
// React ML app
[]byte(`==(null===(t=e.components)||void 0===t?void 0:t.QueryCtrl)};function`),
} {
tcs = append(tcs, tc{
name: "not angular " + strconv.Itoa(i),
plugin: &plugins.Plugin{
FS: plugins.NewInMemoryFS(map[string][]byte{
"module.js": content,
}),
},
exp: false,
})
}
inspector := PatternsListInspector{DetectorsProvider: NewDefaultStaticDetectorsProvider()}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
isAngular, err := inspector.Inspect(context.Background(), tc.plugin)
require.NoError(t, err)
require.Equal(t, tc.exp, isAngular)
})
}
t.Run("no module.js", func(t *testing.T) {
p := &plugins.Plugin{FS: plugins.NewInMemoryFS(map[string][]byte{})}
_, err := inspector.Inspect(context.Background(), p)
require.NoError(t, err)
})
}

@ -1,28 +1,32 @@
package angulardetector package angularinspector
import "github.com/grafana/grafana/pkg/plugins" import (
"context"
"github.com/grafana/grafana/pkg/plugins"
)
// FakeInspector is an inspector whose Inspect function can be set to any function. // FakeInspector is an inspector whose Inspect function can be set to any function.
type FakeInspector struct { type FakeInspector struct {
// InspectFunc is the function called when calling Inspect() // InspectFunc is the function called when calling Inspect()
InspectFunc func(p *plugins.Plugin) (bool, error) InspectFunc func(ctx context.Context, p *plugins.Plugin) (bool, error)
} }
func (i *FakeInspector) Inspect(p *plugins.Plugin) (bool, error) { func (i *FakeInspector) Inspect(ctx context.Context, p *plugins.Plugin) (bool, error) {
return i.InspectFunc(p) return i.InspectFunc(ctx, p)
} }
var ( var (
// AlwaysAngularFakeInspector is an inspector that always returns `true, nil` // AlwaysAngularFakeInspector is an inspector that always returns `true, nil`
AlwaysAngularFakeInspector = &FakeInspector{ AlwaysAngularFakeInspector = &FakeInspector{
InspectFunc: func(p *plugins.Plugin) (bool, error) { InspectFunc: func(_ context.Context, _ *plugins.Plugin) (bool, error) {
return true, nil return true, nil
}, },
} }
// NeverAngularFakeInspector is an inspector that always returns `false, nil` // NeverAngularFakeInspector is an inspector that always returns `false, nil`
NeverAngularFakeInspector = &FakeInspector{ NeverAngularFakeInspector = &FakeInspector{
InspectFunc: func(p *plugins.Plugin) (bool, error) { InspectFunc: func(_ context.Context, _ *plugins.Plugin) (bool, error) {
return false, nil return false, nil
}, },
} }

@ -0,0 +1,35 @@
package angularinspector
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/stretchr/testify/require"
)
func TestFakeInspector(t *testing.T) {
t.Run("FakeInspector", func(t *testing.T) {
var called bool
inspector := FakeInspector{InspectFunc: func(_ context.Context, _ *plugins.Plugin) (bool, error) {
called = true
return false, nil
}}
r, err := inspector.Inspect(context.Background(), &plugins.Plugin{})
require.True(t, called)
require.NoError(t, err)
require.False(t, r)
})
t.Run("AlwaysAngularFakeInspector", func(t *testing.T) {
r, err := AlwaysAngularFakeInspector.Inspect(context.Background(), &plugins.Plugin{})
require.NoError(t, err)
require.True(t, r)
})
t.Run("NeverAngularFakeInspector", func(t *testing.T) {
r, err := NeverAngularFakeInspector.Inspect(context.Background(), &plugins.Plugin{})
require.NoError(t, err)
require.False(t, r)
})
}

@ -1,46 +0,0 @@
package angulardetector
import (
"bytes"
"regexp"
"github.com/grafana/grafana/pkg/plugins"
)
var (
_ detector = &containsBytesDetector{}
_ detector = &regexDetector{}
)
// detector implements a check to see if a plugin uses Angular.
type detector interface {
// Detect takes the content of a moduleJs file and returns true if the plugin is using Angular.
Detect(moduleJs []byte) bool
}
// containsBytesDetector is a detector that returns true if module.js contains the "pattern" string.
type containsBytesDetector struct {
pattern []byte
}
// Detect returns true if moduleJs contains the byte slice d.pattern.
func (d *containsBytesDetector) Detect(moduleJs []byte) bool {
return bytes.Contains(moduleJs, d.pattern)
}
// regexDetector is a detector that returns true if the module.js content matches a regular expression.
type regexDetector struct {
regex *regexp.Regexp
}
// Detect returns true if moduleJs matches the regular expression d.regex.
func (d *regexDetector) Detect(moduleJs []byte) bool {
return d.regex.Match(moduleJs)
}
// Inspector can inspect a module.js and determine if it's an Angular plugin or not.
type Inspector interface {
// Inspect open module.js and checks if the plugin is using Angular by matching against its source code.
// It returns true if module.js matches against any of the detectors in angularDetectors.
Inspect(p *plugins.Plugin) (bool, error)
}

@ -1,95 +0,0 @@
package angulardetector
import (
"strconv"
"testing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/stretchr/testify/require"
)
func TestAngularDetector_Inspect(t *testing.T) {
type tc struct {
name string
plugin *plugins.Plugin
exp bool
}
var tcs []tc
// Angular imports
for i, content := range [][]byte{
[]byte(`import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk';`),
[]byte(`define(["app/plugins/sdk"],(function(n){return function(n){var t={};function e(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return n[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=n,e.c=t,e.d=function(n,t,r){e.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:r})},e.r=function(n){"undefined"!=typeof`),
[]byte(`define(["app/plugins/sdk"],(function(n){return function(n){var t={};function e(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return n[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=n,e.c=t,e.d=function(n,t,r){e.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:r})},e.r=function(n){"undefined"!=typeof Symbol&&Symbol.toSt`),
[]byte(`define(["react","lodash","@grafana/data","@grafana/ui","@emotion/css","@grafana/runtime","moment","app/core/utils/datemath","jquery","app/plugins/sdk","app/core/core_module","app/core/core","app/core/table_model","app/core/utils/kbn","app/core/config","angular"],(function(e,t,r,n,i,a,o,s,u,l,c,p,f,h,d,m){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};retur`),
[]byte(`exports_1("QueryCtrl", query_ctrl_1.PluginQueryCtrl);`),
[]byte(`exports_1('QueryCtrl', query_ctrl_1.PluginQueryCtrl);`),
} {
tcs = append(tcs, tc{
name: "angular " + strconv.Itoa(i),
plugin: &plugins.Plugin{
FS: plugins.NewInMemoryFS(map[string][]byte{
"module.js": content,
}),
},
exp: true,
})
}
// Not angular (test against possible false detections)
for i, content := range [][]byte{
[]byte(`import { PanelPlugin } from '@grafana/data'`),
// React ML app
[]byte(`==(null===(t=e.components)||void 0===t?void 0:t.QueryCtrl)};function`),
} {
tcs = append(tcs, tc{
name: "not angular " + strconv.Itoa(i),
plugin: &plugins.Plugin{
FS: plugins.NewInMemoryFS(map[string][]byte{
"module.js": content,
}),
},
exp: false,
})
}
inspector := NewDefaultPatternsListInspector()
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
isAngular, err := inspector.Inspect(tc.plugin)
require.NoError(t, err)
require.Equal(t, tc.exp, isAngular)
})
}
t.Run("no module.js", func(t *testing.T) {
p := &plugins.Plugin{FS: plugins.NewInMemoryFS(map[string][]byte{})}
_, err := inspector.Inspect(p)
require.ErrorIs(t, err, plugins.ErrFileNotExist)
})
}
func TestFakeInspector(t *testing.T) {
t.Run("FakeInspector", func(t *testing.T) {
var called bool
inspector := FakeInspector{InspectFunc: func(p *plugins.Plugin) (bool, error) {
called = true
return false, nil
}}
r, err := inspector.Inspect(&plugins.Plugin{})
require.True(t, called)
require.NoError(t, err)
require.False(t, r)
})
t.Run("AlwaysAngularFakeInspector", func(t *testing.T) {
r, err := AlwaysAngularFakeInspector.Inspect(&plugins.Plugin{})
require.NoError(t, err)
require.True(t, r)
})
t.Run("NeverAngularFakeInspector", func(t *testing.T) {
r, err := NeverAngularFakeInspector.Inspect(&plugins.Plugin{})
require.NoError(t, err)
require.False(t, r)
})
}

@ -1,60 +0,0 @@
package angulardetector
import (
"fmt"
"io"
"regexp"
"github.com/grafana/grafana/pkg/plugins"
)
// defaultDetectors contains all the detectors to detect Angular plugins.
// They are executed in the specified order.
var defaultDetectors = []detector{
&containsBytesDetector{pattern: []byte("PanelCtrl")},
&containsBytesDetector{pattern: []byte("ConfigCtrl")},
&containsBytesDetector{pattern: []byte("app/plugins/sdk")},
&containsBytesDetector{pattern: []byte("angular.isNumber(")},
&containsBytesDetector{pattern: []byte("editor.html")},
&containsBytesDetector{pattern: []byte("ctrl.annotation")},
&containsBytesDetector{pattern: []byte("getLegacyAngularInjector")},
&regexDetector{regex: regexp.MustCompile(`["']QueryCtrl["']`)},
}
// PatternsListInspector matches module.js against all the specified patterns, in sequence.
type PatternsListInspector struct {
detectors []detector
}
// NewDefaultPatternsListInspector returns a new *PatternsListInspector using defaultDetectors as detectors.
func NewDefaultPatternsListInspector() *PatternsListInspector {
return &PatternsListInspector{detectors: defaultDetectors}
}
func ProvideService() Inspector {
return NewDefaultPatternsListInspector()
}
func (i *PatternsListInspector) Inspect(p *plugins.Plugin) (isAngular bool, err error) {
f, err := p.FS.Open("module.js")
if err != nil {
return false, fmt.Errorf("open module.js: %w", err)
}
defer func() {
if closeErr := f.Close(); closeErr != nil && err == nil {
err = fmt.Errorf("close module.js: %w", closeErr)
}
}()
b, err := io.ReadAll(f)
if err != nil {
return false, fmt.Errorf("module.js readall: %w", err)
}
for _, d := range i.detectors {
if d.Detect(b) {
isAngular = true
break
}
}
return
}

@ -6,13 +6,14 @@ import (
"fmt" "fmt"
"path" "path"
"strings" "strings"
"time"
"github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/infra/slugify"
"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/loader/angulardetector" "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/loader/initializer" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
@ -36,7 +37,7 @@ type Loader struct {
log log.Logger log log.Logger
cfg *config.Cfg cfg *config.Cfg
angularInspector angulardetector.Inspector angularInspector angularinspector.Inspector
errs map[string]*plugins.SignatureError errs map[string]*plugins.SignatureError
} }
@ -44,7 +45,7 @@ type Loader struct {
func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer,
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginFinder finder.Finder, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginFinder finder.Finder,
roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, signatureCalculator plugins.SignatureCalculator, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, signatureCalculator plugins.SignatureCalculator,
angularInspector angulardetector.Inspector) *Loader { angularInspector angularinspector.Inspector) *Loader {
return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry), return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry),
roleRegistry, assetPath, pluginFinder, signatureCalculator, angularInspector) roleRegistry, assetPath, pluginFinder, signatureCalculator, angularInspector)
} }
@ -53,7 +54,7 @@ func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLo
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider,
processManager process.Service, roleRegistry plugins.RoleRegistry, processManager process.Service, roleRegistry plugins.RoleRegistry,
assetPath *assetpath.Service, pluginFinder finder.Finder, signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service, pluginFinder finder.Finder, signatureCalculator plugins.SignatureCalculator,
angularInspector angulardetector.Inspector) *Loader { angularInspector angularinspector.Inspector) *Loader {
return &Loader{ return &Loader{
pluginFinder: pluginFinder, pluginFinder: pluginFinder,
pluginRegistry: pluginRegistry, pluginRegistry: pluginRegistry,
@ -182,10 +183,14 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun
// initialize plugins // initialize plugins
initializedPlugins := make([]*plugins.Plugin, 0, len(verifiedPlugins)) initializedPlugins := make([]*plugins.Plugin, 0, len(verifiedPlugins))
for _, p := range verifiedPlugins { for _, p := range verifiedPlugins {
// Detect angular for external plugins // detect angular for external plugins
if p.IsExternalPlugin() { if p.IsExternalPlugin() {
var err error var err error
p.AngularDetected, err = l.angularInspector.Inspect(p)
cctx, canc := context.WithTimeout(ctx, time.Minute*1)
p.AngularDetected, err = l.angularInspector.Inspect(cctx, p)
canc()
if err != nil { if err != nil {
l.log.Warn("could not inspect plugin for angular", "pluginID", p.ID, "err", err) l.log.Warn("could not inspect plugin for angular", "pluginID", p.ID, "err", err)
} }

@ -9,6 +9,7 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"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/plugins" "github.com/grafana/grafana/pkg/plugins"
@ -16,7 +17,6 @@ import (
"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/fakes" "github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angulardetector"
"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/loader/initializer" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
@ -438,7 +438,7 @@ 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(tt.cfg, func(l *Loader) { l := newLoader(t, tt.cfg, func(l *Loader) {
l.pluginRegistry = reg l.pluginRegistry = reg
l.processManager = procMgr l.processManager = procMgr
l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, &fakes.FakeLicensingService{}) l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, &fakes.FakeLicensingService{})
@ -521,7 +521,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(cfg) l := newLoader(t, cfg)
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
@ -672,7 +672,7 @@ 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(tt.cfg, func(l *Loader) { l := newLoader(t, tt.cfg, func(l *Loader) {
l.pluginRegistry = reg l.pluginRegistry = reg
l.processManager = procMgr l.processManager = procMgr
l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, fakes.NewFakeLicensingService()) l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, fakes.NewFakeLicensingService())
@ -793,7 +793,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(tt.cfg, func(l *Loader) { l := newLoader(t, tt.cfg, func(l *Loader) {
l.pluginRegistry = reg l.pluginRegistry = reg
l.processManager = procMgr l.processManager = procMgr
l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, fakes.NewFakeLicensingService()) l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, fakes.NewFakeLicensingService())
@ -872,7 +872,7 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) {
reg := fakes.NewFakePluginRegistry() reg := fakes.NewFakePluginRegistry()
procPrvdr := fakes.NewFakeBackendProcessProvider() procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
l := newLoader(&config.Cfg{}, func(l *Loader) { l := newLoader(t, &config.Cfg{}, func(l *Loader) {
l.pluginRegistry = reg l.pluginRegistry = reg
l.processManager = procMgr l.processManager = procMgr
l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService())
@ -956,7 +956,7 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
reg := fakes.NewFakePluginRegistry() reg := fakes.NewFakePluginRegistry()
procPrvdr := fakes.NewFakeBackendProcessProvider() procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
l := newLoader(&config.Cfg{}, func(l *Loader) { l := newLoader(t, &config.Cfg{}, func(l *Loader) {
l.pluginRegistry = reg l.pluginRegistry = reg
l.processManager = procMgr l.processManager = procMgr
l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService())
@ -1055,7 +1055,7 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
} }
} }
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
l := newLoader(&config.Cfg{}, func(l *Loader) { l := newLoader(t, &config.Cfg{}, func(l *Loader) {
l.pluginRegistry = reg l.pluginRegistry = reg
l.processManager = procMgr l.processManager = procMgr
l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService())
@ -1097,24 +1097,24 @@ func TestLoader_Load_Angular(t *testing.T) {
t.Run(cfgTc.name, func(t *testing.T) { t.Run(cfgTc.name, func(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
name string name string
angularInspector angulardetector.Inspector angularInspector angularinspector.Inspector
shouldLoad bool shouldLoad bool
}{ }{
{ {
name: "angular plugin", name: "angular plugin",
angularInspector: angulardetector.AlwaysAngularFakeInspector, angularInspector: angularinspector.AlwaysAngularFakeInspector,
// angular plugins should load only if allowed by the cfg // angular plugins should load only if allowed by the cfg
shouldLoad: cfgTc.cfg.AngularSupportEnabled, shouldLoad: cfgTc.cfg.AngularSupportEnabled,
}, },
{ {
name: "non angular plugin", name: "non angular plugin",
angularInspector: angulardetector.NeverAngularFakeInspector, angularInspector: angularinspector.NeverAngularFakeInspector,
// non-angular plugins should always load // non-angular plugins should always load
shouldLoad: true, shouldLoad: true,
}, },
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
l := newLoader(cfgTc.cfg, func(l *Loader) { l := newLoader(t, cfgTc.cfg, func(l *Loader) {
l.angularInspector = tc.angularInspector l.angularInspector = tc.angularInspector
}) })
p, err := l.Load(context.Background(), fakePluginSource) p, err := l.Load(context.Background(), fakePluginSource)
@ -1208,7 +1208,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
reg := fakes.NewFakePluginRegistry() reg := fakes.NewFakePluginRegistry()
procPrvdr := fakes.NewFakeBackendProcessProvider() procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
l := newLoader(&config.Cfg{}, func(l *Loader) { l := newLoader(t, &config.Cfg{}, func(l *Loader) {
l.pluginRegistry = reg l.pluginRegistry = reg
l.processManager = procMgr l.processManager = procMgr
l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService())
@ -1386,7 +1386,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
reg := fakes.NewFakePluginRegistry() reg := fakes.NewFakePluginRegistry()
procPrvdr := fakes.NewFakeBackendProcessProvider() procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
l := newLoader(&config.Cfg{}, func(l *Loader) { l := newLoader(t, &config.Cfg{}, func(l *Loader) {
l.pluginRegistry = reg l.pluginRegistry = reg
l.processManager = procMgr l.processManager = procMgr
l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService())
@ -1437,11 +1437,13 @@ func Test_setPathsBasedOnApp(t *testing.T) {
}) })
} }
func newLoader(cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader { func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader {
angularInspector, err := angularinspector.NewStaticInspector()
require.NoError(t, err)
l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(),
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg),
signature.ProvideService(cfg, statickey.New()), angulardetector.NewDefaultPatternsListInspector()) signature.ProvideService(cfg, statickey.New()), angularInspector)
for _, cb := range cbs { for _, cb := range cbs {
cb(l) cb(l)

@ -10,6 +10,7 @@ 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-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
@ -21,7 +22,6 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/client" "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"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angulardetector"
"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/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
@ -118,10 +118,12 @@ func TestIntegrationPluginManager(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
reg := registry.ProvideService() reg := registry.ProvideService()
lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg}) lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg})
angularInspector, err := angularinspector.NewStaticInspector()
require.NoError(t, err)
l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg), l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg),
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(), reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, statickey.New()),
angulardetector.NewDefaultPatternsListInspector()) angularInspector)
srcs := sources.ProvideService(cfg, pCfg) srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l) ps, err := store.ProvideService(reg, srcs, l)
require.NoError(t, err) require.NoError(t, err)

@ -565,6 +565,13 @@ var (
Stage: FeatureStageExperimental, Stage: FeatureStageExperimental,
Owner: grafanaObservabilityMetricsSquad, Owner: grafanaObservabilityMetricsSquad,
}, },
{
Name: "pluginsDynamicAngularDetectionPatterns",
Description: "Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones",
Stage: FeatureStageExperimental,
FrontendOnly: false,
Owner: grafanaPluginsPlatformSquad,
},
{ {
Name: "alertingLokiRangeToInstant", Name: "alertingLokiRangeToInstant",
Description: "Rewrites eligible loki range queries to instant queries", Description: "Rewrites eligible loki range queries to instant queries",

@ -82,5 +82,6 @@ sqlDatasourceDatabaseSelection,preview,@grafana/grafana-bi-squad,false,false,fal
cloudWatchLogsMonacoEditor,experimental,@grafana/aws-plugins,false,false,false,true cloudWatchLogsMonacoEditor,experimental,@grafana/aws-plugins,false,false,false,true
exploreScrollableLogsContainer,experimental,@grafana/observability-logs,false,false,false,true exploreScrollableLogsContainer,experimental,@grafana/observability-logs,false,false,false,true
recordedQueriesMulti,experimental,@grafana/observability-metrics,false,false,false,false recordedQueriesMulti,experimental,@grafana/observability-metrics,false,false,false,false
pluginsDynamicAngularDetectionPatterns,experimental,@grafana/plugins-platform-backend,false,false,false,false
alertingLokiRangeToInstant,experimental,@grafana/alerting-squad,false,false,false,false alertingLokiRangeToInstant,experimental,@grafana/alerting-squad,false,false,false,false
flameGraphV2,experimental,@grafana/observability-traces-and-profiling,false,false,false,true flameGraphV2,experimental,@grafana/observability-traces-and-profiling,false,false,false,true

1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
82 cloudWatchLogsMonacoEditor experimental @grafana/aws-plugins false false false true
83 exploreScrollableLogsContainer experimental @grafana/observability-logs false false false true
84 recordedQueriesMulti experimental @grafana/observability-metrics false false false false
85 pluginsDynamicAngularDetectionPatterns experimental @grafana/plugins-platform-backend false false false false
86 alertingLokiRangeToInstant experimental @grafana/alerting-squad false false false false
87 flameGraphV2 experimental @grafana/observability-traces-and-profiling false false false true

@ -339,6 +339,10 @@ const (
// Enables writing multiple items from a single query within Recorded Queries // Enables writing multiple items from a single query within Recorded Queries
FlagRecordedQueriesMulti = "recordedQueriesMulti" FlagRecordedQueriesMulti = "recordedQueriesMulti"
// FlagPluginsDynamicAngularDetectionPatterns
// Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones
FlagPluginsDynamicAngularDetectionPatterns = "pluginsDynamicAngularDetectionPatterns"
// FlagAlertingLokiRangeToInstant // FlagAlertingLokiRangeToInstant
// Rewrites eligible loki range queries to instant queries // Rewrites eligible loki range queries to instant queries
FlagAlertingLokiRangeToInstant = "alertingLokiRangeToInstant" FlagAlertingLokiRangeToInstant = "alertingLokiRangeToInstant"

@ -0,0 +1,157 @@
package angulardetector
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector"
)
const (
// gcomAngularPatternsPath is the relative path to the GCOM API handler that returns angular detection patterns.
gcomAngularPatternsPath = "/api/plugins/angular_patterns"
)
var _ angulardetector.DetectorsProvider = &GCOMDetectorsProvider{}
// GCOMDetectorsProvider is a DetectorsProvider which fetches patterns from GCOM.
type GCOMDetectorsProvider struct {
log log.Logger
httpClient *http.Client
baseURL string
}
// NewGCOMDetectorsProvider returns a new GCOMDetectorsProvider.
// baseURL is the GCOM base url, without /api and without a trailing slash (e.g.: https://grafana.com)
func NewGCOMDetectorsProvider(baseURL string) (angulardetector.DetectorsProvider, error) {
cl, err := httpclient.New()
if err != nil {
return nil, fmt.Errorf("httpclient new: %w", err)
}
return &GCOMDetectorsProvider{
log: log.New("plugins.angulardetector.gcom"),
baseURL: baseURL,
httpClient: cl,
}, nil
}
// ProvideDetectors gets the dynamic angular detectors from the remote source.
// If an error occurs, the function fails silently by logging an error, and it returns nil.
func (p *GCOMDetectorsProvider) ProvideDetectors(ctx context.Context) []angulardetector.AngularDetector {
patterns, err := p.fetch(ctx)
if err != nil {
p.log.Warn("Could not fetch remote angular patterns", "error", err)
return nil
}
detectors, err := p.patternsToDetectors(patterns)
if err != nil {
p.log.Warn("Could not convert angular patterns to angularDetectors", "error", err)
return nil
}
return detectors
}
// fetch fetches the angular patterns from GCOM and returns them as gcomPatterns.
// Call angularDetectors() on the returned value to get the corresponding angular detectors.
func (p *GCOMDetectorsProvider) fetch(ctx context.Context) (gcomPatterns, error) {
st := time.Now()
reqURL, err := url.JoinPath(p.baseURL, gcomAngularPatternsPath)
if err != nil {
return nil, fmt.Errorf("url joinpath: %w", err)
}
p.log.Debug("Fetching dynamic angular detection patterns", "url", reqURL)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return nil, fmt.Errorf("new request with context: %w", err)
}
resp, err := p.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("http do: %w", err)
}
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
p.log.Error("response body close error", "error", err)
}
}()
var out gcomPatterns
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("json decode: %w", err)
}
p.log.Debug("Fetched dynamic angular detection patterns", "patterns", len(out), "duration", time.Since(st))
return out, nil
}
// patternsToDetectors converts a slice of gcomPattern into a slice of angulardetector.AngularDetector, by calling
// angularDetector() on each gcomPattern.
func (p *GCOMDetectorsProvider) patternsToDetectors(patterns gcomPatterns) ([]angulardetector.AngularDetector, error) {
var finalErr error
detectors := make([]angulardetector.AngularDetector, 0, len(patterns))
for _, pattern := range patterns {
d, err := pattern.angularDetector()
if err != nil {
// Fail silently in case of an errUnknownPatternType.
// This allows us to introduce new pattern types without breaking old Grafana versions
if errors.Is(err, errUnknownPatternType) {
p.log.Debug("Unknown angular pattern", "name", pattern.Name, "type", pattern.Type, "error", err)
continue
}
// Other error, do not ignore it
finalErr = errors.Join(finalErr, err)
}
detectors = append(detectors, d)
}
if finalErr != nil {
return nil, finalErr
}
return detectors, nil
}
// gcomPatternType is a pattern type returned by the GCOM API.
type gcomPatternType string
const (
gcomPatternTypeContains gcomPatternType = "contains"
gcomPatternTypeRegex gcomPatternType = "regex"
)
// errUnknownPatternType is returned when a pattern type is not known.
var errUnknownPatternType = errors.New("unknown pattern type")
// gcomPattern is an Angular detection pattern returned by the GCOM API.
type gcomPattern struct {
Name string
Pattern string
Type gcomPatternType
}
// angularDetector converts a gcomPattern into an AngularDetector, based on its Type.
// If a pattern type is unknown, it returns an error wrapping errUnknownPatternType.
func (p *gcomPattern) angularDetector() (angulardetector.AngularDetector, error) {
switch p.Type {
case gcomPatternTypeContains:
return &angulardetector.ContainsBytesDetector{Pattern: []byte(p.Pattern)}, nil
case gcomPatternTypeRegex:
re, err := regexp.Compile(p.Pattern)
if err != nil {
return nil, fmt.Errorf("%q regexp compile: %w", p.Pattern, err)
}
return &angulardetector.RegexDetector{Regex: re}, nil
}
return nil, fmt.Errorf("%q: %w", p.Type, errUnknownPatternType)
}
// gcomPatterns is a slice of gcomPattern s.
type gcomPatterns []gcomPattern

@ -0,0 +1,144 @@
package angulardetector
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector"
)
var mockGCOMResponse = []byte(`[{
"name": "PanelCtrl",
"type": "contains",
"pattern": "PanelCtrl"
},
{
"name": "QueryCtrl",
"type": "regex",
"pattern": "[\"']QueryCtrl[\"']"
}]`)
func mockGCOMHTTPHandlerFunc(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/api/plugins/angular_patterns" {
writer.WriteHeader(http.StatusNotFound)
return
}
_, _ = writer.Write(mockGCOMResponse)
}
func checkMockGCOMResponse(t *testing.T, detectors []angulardetector.AngularDetector) {
require.Len(t, detectors, 2)
d, ok := detectors[0].(*angulardetector.ContainsBytesDetector)
require.True(t, ok)
require.Equal(t, []byte(`PanelCtrl`), d.Pattern)
rd, ok := detectors[1].(*angulardetector.RegexDetector)
require.True(t, ok)
require.Equal(t, `["']QueryCtrl["']`, rd.Regex.String())
}
type gcomScenario struct {
gcomHTTPHandlerFunc http.HandlerFunc
gcomHTTPCalls int
}
func (s *gcomScenario) newHTTPTestServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.gcomHTTPCalls++
s.gcomHTTPHandlerFunc(w, r)
}))
}
func newDefaultGCOMScenario() *gcomScenario {
return &gcomScenario{gcomHTTPHandlerFunc: mockGCOMHTTPHandlerFunc}
}
func TestGCOMDetectorsProvider(t *testing.T) {
t.Run("returns value returned from gcom api", func(t *testing.T) {
scenario := newDefaultGCOMScenario()
srv := scenario.newHTTPTestServer()
t.Cleanup(srv.Close)
gcomProvider, err := NewGCOMDetectorsProvider(srv.URL)
require.NoError(t, err)
detectors := gcomProvider.ProvideDetectors(context.Background())
require.Equal(t, 1, scenario.gcomHTTPCalls, "gcom api should be called")
checkMockGCOMResponse(t, detectors)
})
t.Run("error handling", func(t *testing.T) {
for _, tc := range []struct {
*gcomScenario
name string
}{
{name: "http error 500", gcomScenario: &gcomScenario{
gcomHTTPHandlerFunc: func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusInternalServerError)
},
}},
{name: "invalid json", gcomScenario: &gcomScenario{
gcomHTTPHandlerFunc: func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write([]byte(`not json`))
},
}},
{name: "invalid regex", gcomScenario: &gcomScenario{
gcomHTTPHandlerFunc: func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write([]byte(`[{"name": "test", "type": "regex", "pattern": "((("}]`))
},
}},
} {
t.Run(tc.name, func(t *testing.T) {
srv := tc.newHTTPTestServer()
t.Cleanup(srv.Close)
gcomProvider, err := NewGCOMDetectorsProvider(srv.URL)
require.NoError(t, err)
detectors := gcomProvider.ProvideDetectors(context.Background())
require.Equal(t, 1, tc.gcomHTTPCalls, "gcom should be called")
require.Empty(t, detectors, "returned AngularDetectors should be empty")
})
}
})
t.Run("handles gcom timeout", func(t *testing.T) {
gcomScenario := &gcomScenario{
gcomHTTPHandlerFunc: func(writer http.ResponseWriter, request *http.Request) {
time.Sleep(time.Second * 1)
_, _ = writer.Write([]byte(`[{"name": "test", "type": "regex", "pattern": "((("}]`))
},
}
srv := gcomScenario.newHTTPTestServer()
t.Cleanup(srv.Close)
gcomProvider, err := NewGCOMDetectorsProvider(srv.URL)
require.NoError(t, err)
// Expired context
ctx, canc := context.WithTimeout(context.Background(), time.Second*-1)
defer canc()
detectors := gcomProvider.ProvideDetectors(ctx)
require.Zero(t, gcomScenario.gcomHTTPCalls, "gcom should be not called due to request timing out")
require.Empty(t, detectors, "returned AngularDetectors should be empty")
})
t.Run("unknown pattern types do not break decoding", func(t *testing.T) {
// Tests that we can introduce new pattern types in the future without breaking old Grafana versions.
scenario := gcomScenario{gcomHTTPHandlerFunc: func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write([]byte(`[
{"name": "PanelCtrl", "type": "contains", "pattern": "PanelCtrl"},
{"name": "Another", "type": "unknown", "pattern": "PanelCtrl"}
]`))
}}
srv := scenario.newHTTPTestServer()
t.Cleanup(srv.Close)
gcomProvider, err := NewGCOMDetectorsProvider(srv.URL)
require.NoError(t, err)
detectors := gcomProvider.ProvideDetectors(context.Background())
require.Equal(t, 1, scenario.gcomHTTPCalls, "gcom should be called")
require.Len(t, detectors, 1, "should have decoded only 1 AngularDetector")
d, ok := detectors[0].(*angulardetector.ContainsBytesDetector)
require.True(t, ok, "decoded pattern should be of the correct type")
require.Equal(t, []byte("PanelCtrl"), d.Pattern, "decoded value for known pattern should be correct")
})
}

@ -0,0 +1,45 @@
package angularinspector
import (
"fmt"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/services/featuremgmt"
pAngularDetector "github.com/grafana/grafana/pkg/services/pluginsintegration/angulardetector"
)
type Service struct {
angularinspector.Inspector
}
// newDynamicInspector returns the default dynamic Inspector, which is a PatternsListInspector that will:
// 1. Try to get the Angular detectors from GCOM
// 2. If it fails, it will use the static (hardcoded) detections provided by defaultDetectors.
func newDynamicInspector(cfg *config.Cfg) (angularinspector.Inspector, error) {
dynamicProvider, err := pAngularDetector.NewGCOMDetectorsProvider(cfg.GrafanaComURL)
if err != nil {
return nil, fmt.Errorf("NewGCOMDetectorsProvider: %w", err)
}
return &angularinspector.PatternsListInspector{
DetectorsProvider: angulardetector.SequenceDetectorsProvider{
dynamicProvider,
angularinspector.NewDefaultStaticDetectorsProvider(),
},
}, nil
}
func ProvideService(cfg *config.Cfg) (*Service, error) {
var underlying angularinspector.Inspector
var err error
if cfg.Features != nil && cfg.Features.IsEnabled(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns) {
underlying, err = newDynamicInspector(cfg)
} else {
underlying, err = angularinspector.NewStaticInspector()
}
if err != nil {
return nil, err
}
return &Service{underlying}, nil
}

@ -0,0 +1,42 @@
package angularinspector
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/services/featuremgmt"
pAngularDetector "github.com/grafana/grafana/pkg/services/pluginsintegration/angulardetector"
)
func TestProvideService(t *testing.T) {
t.Run("uses hardcoded inspector if feature flag is not present", func(t *testing.T) {
inspector, err := ProvideService(&config.Cfg{
Features: featuremgmt.WithFeatures(),
})
require.NoError(t, err)
require.IsType(t, inspector.Inspector, &angularinspector.PatternsListInspector{})
patternsListInspector := inspector.Inspector.(*angularinspector.PatternsListInspector)
detectors := patternsListInspector.DetectorsProvider.ProvideDetectors(context.Background())
require.NotEmpty(t, detectors, "provided detectors should not be empty")
})
t.Run("uses dynamic inspector with hardcoded fallback if feature flag is present", func(t *testing.T) {
inspector, err := ProvideService(&config.Cfg{
Features: featuremgmt.WithFeatures(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns),
})
require.NoError(t, err)
require.IsType(t, inspector.Inspector, &angularinspector.PatternsListInspector{})
require.IsType(t, inspector.Inspector.(*angularinspector.PatternsListInspector).DetectorsProvider, angulardetector.SequenceDetectorsProvider{})
seq := inspector.Inspector.(*angularinspector.PatternsListInspector).DetectorsProvider.(angulardetector.SequenceDetectorsProvider)
require.Len(t, seq, 2, "should return the correct number of providers")
require.IsType(t, seq[0], &pAngularDetector.GCOMDetectorsProvider{}, "first AngularDetector provided should be gcom")
require.IsType(t, seq[1], &angulardetector.StaticDetectorsProvider{}, "second AngularDetector provided should be static")
staticDetectors := seq[1].ProvideDetectors(context.Background())
require.NotEmpty(t, staticDetectors, "provided static detectors should not be empty")
})
}

@ -42,6 +42,7 @@ func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg, fe
tracingCfg, tracingCfg,
featuremgmt.ProvideToggles(features), featuremgmt.ProvideToggles(features),
grafanaCfg.AngularSupportEnabled, grafanaCfg.AngularSupportEnabled,
grafanaCfg.GrafanaComURL,
), nil ), nil
} }

@ -12,7 +12,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/client"
"github.com/grafana/grafana/pkg/plugins/manager/filestore" "github.com/grafana/grafana/pkg/plugins/manager/filestore"
"github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angulardetector" pAngularInspector "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/process" "github.com/grafana/grafana/pkg/plugins/manager/process"
@ -25,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/services/caching" "github.com/grafana/grafana/pkg/services/caching"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/pluginsintegration/angularinspector"
"github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware" "github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware"
"github.com/grafana/grafana/pkg/services/pluginsintegration/config" "github.com/grafana/grafana/pkg/services/pluginsintegration/config"
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever" "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever"
@ -52,7 +53,8 @@ var WireSet = wire.NewSet(
coreplugin.ProvideCoreRegistry, coreplugin.ProvideCoreRegistry,
pluginscdn.ProvideService, pluginscdn.ProvideService,
assetpath.ProvideService, assetpath.ProvideService,
angulardetector.ProvideService, angularinspector.ProvideService,
wire.Bind(new(pAngularInspector.Inspector), new(*angularinspector.Service)),
loader.ProvideService, loader.ProvideService,
wire.Bind(new(loader.Service), new(*loader.Loader)), wire.Bind(new(loader.Service), new(*loader.Loader)),
wire.Bind(new(plugins.ErrorResolver), new(*loader.Loader)), wire.Bind(new(plugins.ErrorResolver), new(*loader.Loader)),

Loading…
Cancel
Save