diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index ab783d5eadf..25eaaf0f6eb 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -72,50 +72,51 @@ Some features are enabled by default. You can disable these feature by setting t These features are early in their development lifecycle and so are not yet supported in Grafana Cloud. Experimental features might be changed or removed without prior notice. -| Feature toggle name | Description | -| ---------------------------------- | ------------------------------------------------------------------------------------------------------------ | -| `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 | -| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | -| `storage` | Configurable storage for dashboards, datasources, and resources | -| `newTraceViewHeader` | Shows the new trace view header | -| `datasourceQueryMultiStatus` | Introduce HTTP 207 Multi Status for api/ds/query | -| `traceToMetrics` | Enable trace to metrics links | -| `prometheusWideSeries` | Enable wide series responses in the Prometheus datasource | -| `canvasPanelNesting` | Allow elements nesting | -| `scenes` | Experimental framework to build interactive dashboards | -| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | -| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | -| `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift | -| `athenaAsyncQueryDataSupport` | Enable async query data support for Athena | -| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | -| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | -| `showTraceId` | Show trace ids for requests | -| `alertingBacktesting` | Rule backtesting API for alerting | -| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | -| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | -| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | -| `individualCookiePreferences` | Support overriding cookie preferences per user | -| `onlyExternalOrgRoleSync` | Prohibits a user from changing organization roles synced with external auth providers | -| `traceqlSearch` | Enables the 'TraceQL Search' tab for the Tempo datasource which provides a UI to generate TraceQL queries | -| `timeSeriesTable` | Enable time series table transformer & sparkline cell type | -| `prometheusResourceBrowserCache` | Displays browser caching options in Prometheus data source configuration | -| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | -| `clientTokenRotation` | Replaces the current in-request token rotation so that the client initiates the rotation | -| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | -| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | -| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | -| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | -| `unifiedRequestLog` | Writes error logs to the request logger | -| `pyroscopeFlameGraph` | Changes flame graph to pyroscope one | -| `extraThemes` | Enables extra themes | -| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | -| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox | -| `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries | -| `exploreScrollableLogsContainer` | Improves the scrolling behavior of logs in Explore | -| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | -| `alertingLokiRangeToInstant` | Rewrites eligible loki range queries to instant queries | -| `flameGraphV2` | New version of flame graph with new features | +| Feature toggle name | Description | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `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 | +| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | +| `storage` | Configurable storage for dashboards, datasources, and resources | +| `newTraceViewHeader` | Shows the new trace view header | +| `datasourceQueryMultiStatus` | Introduce HTTP 207 Multi Status for api/ds/query | +| `traceToMetrics` | Enable trace to metrics links | +| `prometheusWideSeries` | Enable wide series responses in the Prometheus datasource | +| `canvasPanelNesting` | Allow elements nesting | +| `scenes` | Experimental framework to build interactive dashboards | +| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | +| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | +| `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift | +| `athenaAsyncQueryDataSupport` | Enable async query data support for Athena | +| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | +| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | +| `showTraceId` | Show trace ids for requests | +| `alertingBacktesting` | Rule backtesting API for alerting | +| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | +| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | +| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | +| `individualCookiePreferences` | Support overriding cookie preferences per user | +| `onlyExternalOrgRoleSync` | Prohibits a user from changing organization roles synced with external auth providers | +| `traceqlSearch` | Enables the 'TraceQL Search' tab for the Tempo datasource which provides a UI to generate TraceQL queries | +| `timeSeriesTable` | Enable time series table transformer & sparkline cell type | +| `prometheusResourceBrowserCache` | Displays browser caching options in Prometheus data source configuration | +| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | +| `clientTokenRotation` | Replaces the current in-request token rotation so that the client initiates the rotation | +| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | +| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | +| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | +| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | +| `unifiedRequestLog` | Writes error logs to the request logger | +| `pyroscopeFlameGraph` | Changes flame graph to pyroscope one | +| `extraThemes` | Enables extra themes | +| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | +| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox | +| `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries | +| `exploreScrollableLogsContainer` | Improves the scrolling behavior of logs in Explore | +| `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 | +| `flameGraphV2` | New version of flame graph with new features | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b1473d8f672..c61b7101bb2 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -101,6 +101,7 @@ export interface FeatureToggles { cloudWatchLogsMonacoEditor?: boolean; exploreScrollableLogsContainer?: boolean; recordedQueriesMulti?: boolean; + pluginsDynamicAngularDetectionPatterns?: boolean; alertingLokiRangeToInstant?: boolean; flameGraphV2?: boolean; } diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index 42cd6515e7c..66d69f50215 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "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/grafana/grafana/pkg/infra/db" @@ -22,7 +23,6 @@ import ( pluginClient "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/angulardetector" "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/registry" @@ -67,10 +67,12 @@ func TestCallResource(t *testing.T) { 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) l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg), reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, statickey.New()), - angulardetector.NewDefaultPatternsListInspector()) + angularInspector) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/plugins/config/config.go b/pkg/plugins/config/config.go index 4a6b11ecd02..022192befce 100644 --- a/pkg/plugins/config/config.go +++ b/pkg/plugins/config/config.go @@ -45,7 +45,8 @@ type Cfg struct { func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string, 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{ log: log.New("plugin.cfg"), PluginsPath: pluginsPath, @@ -60,7 +61,7 @@ func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSetti LogDatasourceRequests: logDatasourceRequests, PluginsCDNURLTemplate: pluginsCDNURLTemplate, Tracing: tracing, - GrafanaComURL: "https://grafana.com", + GrafanaComURL: grafanaComURL, Features: features, AngularSupportEnabled: angularSupportEnabled, } diff --git a/pkg/plugins/manager/loader/angular/angulardetector/angulardetector.go b/pkg/plugins/manager/loader/angular/angulardetector/angulardetector.go new file mode 100644 index 00000000000..7738c96ea29 --- /dev/null +++ b/pkg/plugins/manager/loader/angular/angulardetector/angulardetector.go @@ -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 +} diff --git a/pkg/plugins/manager/loader/angular/angulardetector/angulardetector_test.go b/pkg/plugins/manager/loader/angular/angulardetector/angulardetector_test.go new file mode 100644 index 00000000000..816c6c937a9 --- /dev/null +++ b/pkg/plugins/manager/loader/angular/angulardetector/angulardetector_test.go @@ -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) + }) + } +} diff --git a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go new file mode 100644 index 00000000000..dd28a5d8076 --- /dev/null +++ b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go @@ -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 +} diff --git a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go new file mode 100644 index 00000000000..c2ba0e0c260 --- /dev/null +++ b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go @@ -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) + }) +} diff --git a/pkg/plugins/manager/loader/angulardetector/fakes.go b/pkg/plugins/manager/loader/angular/angularinspector/fakes.go similarity index 53% rename from pkg/plugins/manager/loader/angulardetector/fakes.go rename to pkg/plugins/manager/loader/angular/angularinspector/fakes.go index eac9e26a1d7..a16c249540e 100644 --- a/pkg/plugins/manager/loader/angulardetector/fakes.go +++ b/pkg/plugins/manager/loader/angular/angularinspector/fakes.go @@ -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. type FakeInspector struct { // 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) { - return i.InspectFunc(p) +func (i *FakeInspector) Inspect(ctx context.Context, p *plugins.Plugin) (bool, error) { + return i.InspectFunc(ctx, p) } var ( // AlwaysAngularFakeInspector is an inspector that always returns `true, nil` AlwaysAngularFakeInspector = &FakeInspector{ - InspectFunc: func(p *plugins.Plugin) (bool, error) { + InspectFunc: func(_ context.Context, _ *plugins.Plugin) (bool, error) { return true, nil }, } // NeverAngularFakeInspector is an inspector that always returns `false, nil` NeverAngularFakeInspector = &FakeInspector{ - InspectFunc: func(p *plugins.Plugin) (bool, error) { + InspectFunc: func(_ context.Context, _ *plugins.Plugin) (bool, error) { return false, nil }, } diff --git a/pkg/plugins/manager/loader/angular/angularinspector/fakes_test.go b/pkg/plugins/manager/loader/angular/angularinspector/fakes_test.go new file mode 100644 index 00000000000..4fc5ee55063 --- /dev/null +++ b/pkg/plugins/manager/loader/angular/angularinspector/fakes_test.go @@ -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) + }) +} diff --git a/pkg/plugins/manager/loader/angulardetector/angulardetector.go b/pkg/plugins/manager/loader/angulardetector/angulardetector.go deleted file mode 100644 index 6180a6b6585..00000000000 --- a/pkg/plugins/manager/loader/angulardetector/angulardetector.go +++ /dev/null @@ -1,46 +0,0 @@ -package angulardetector - -import ( - "bytes" - "regexp" - - "github.com/grafana/grafana/pkg/plugins" -) - -var ( - _ detector = &containsBytesDetector{} - _ detector = ®exDetector{} -) - -// 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) -} diff --git a/pkg/plugins/manager/loader/angulardetector/angulardetector_test.go b/pkg/plugins/manager/loader/angulardetector/angulardetector_test.go deleted file mode 100644 index c50dca73e98..00000000000 --- a/pkg/plugins/manager/loader/angulardetector/angulardetector_test.go +++ /dev/null @@ -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) - }) -} diff --git a/pkg/plugins/manager/loader/angulardetector/service.go b/pkg/plugins/manager/loader/angulardetector/service.go deleted file mode 100644 index c8633a483c8..00000000000 --- a/pkg/plugins/manager/loader/angulardetector/service.go +++ /dev/null @@ -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")}, - - ®exDetector{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 -} diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 79a5beadcb8..aedb7171d2e 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -6,13 +6,14 @@ import ( "fmt" "path" "strings" + "time" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/slugify" "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/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/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" @@ -36,7 +37,7 @@ type Loader struct { log log.Logger cfg *config.Cfg - angularInspector angulardetector.Inspector + angularInspector angularinspector.Inspector errs map[string]*plugins.SignatureError } @@ -44,7 +45,7 @@ type Loader struct { func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginFinder finder.Finder, 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), 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, processManager process.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, pluginFinder finder.Finder, signatureCalculator plugins.SignatureCalculator, - angularInspector angulardetector.Inspector) *Loader { + angularInspector angularinspector.Inspector) *Loader { return &Loader{ pluginFinder: pluginFinder, pluginRegistry: pluginRegistry, @@ -182,10 +183,14 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun // initialize plugins initializedPlugins := make([]*plugins.Plugin, 0, len(verifiedPlugins)) for _, p := range verifiedPlugins { - // Detect angular for external plugins + // detect angular for external plugins if p.IsExternalPlugin() { 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 { l.log.Warn("could not inspect plugin for angular", "pluginID", p.ID, "err", err) } diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index f8efc14acea..c3f00ade9df 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -9,6 +9,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/plugins" @@ -16,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "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/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" @@ -438,7 +438,7 @@ func TestLoader_Load(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - l := newLoader(tt.cfg, func(l *Loader) { + l := newLoader(t, tt.cfg, func(l *Loader) { l.pluginRegistry = reg l.processManager = procMgr 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", }} - l := newLoader(cfg) + l := newLoader(t, cfg) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassBundled @@ -672,7 +672,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - l := newLoader(tt.cfg, func(l *Loader) { + l := newLoader(t, tt.cfg, func(l *Loader) { l.pluginRegistry = reg l.processManager = procMgr l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, fakes.NewFakeLicensingService()) @@ -793,7 +793,7 @@ func TestLoader_Load_RBACReady(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - l := newLoader(tt.cfg, func(l *Loader) { + l := newLoader(t, tt.cfg, func(l *Loader) { l.pluginRegistry = reg l.processManager = procMgr l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, fakes.NewFakeLicensingService()) @@ -872,7 +872,7 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - l := newLoader(&config.Cfg{}, func(l *Loader) { + l := newLoader(t, &config.Cfg{}, func(l *Loader) { l.pluginRegistry = reg l.processManager = procMgr l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) @@ -956,7 +956,7 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - l := newLoader(&config.Cfg{}, func(l *Loader) { + l := newLoader(t, &config.Cfg{}, func(l *Loader) { l.pluginRegistry = reg l.processManager = procMgr l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) @@ -1055,7 +1055,7 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { } } procMgr := fakes.NewFakeProcessManager() - l := newLoader(&config.Cfg{}, func(l *Loader) { + l := newLoader(t, &config.Cfg{}, func(l *Loader) { l.pluginRegistry = reg l.processManager = procMgr 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) { for _, tc := range []struct { name string - angularInspector angulardetector.Inspector + angularInspector angularinspector.Inspector shouldLoad bool }{ { name: "angular plugin", - angularInspector: angulardetector.AlwaysAngularFakeInspector, + angularInspector: angularinspector.AlwaysAngularFakeInspector, // angular plugins should load only if allowed by the cfg shouldLoad: cfgTc.cfg.AngularSupportEnabled, }, { name: "non angular plugin", - angularInspector: angulardetector.NeverAngularFakeInspector, + angularInspector: angularinspector.NeverAngularFakeInspector, // non-angular plugins should always load shouldLoad: true, }, } { 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 }) p, err := l.Load(context.Background(), fakePluginSource) @@ -1208,7 +1208,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - l := newLoader(&config.Cfg{}, func(l *Loader) { + l := newLoader(t, &config.Cfg{}, func(l *Loader) { l.pluginRegistry = reg l.processManager = procMgr l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) @@ -1386,7 +1386,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - l := newLoader(&config.Cfg{}, func(l *Loader) { + l := newLoader(t, &config.Cfg{}, func(l *Loader) { l.pluginRegistry = reg l.processManager = procMgr 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(), fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), 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 { cb(l) diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index 71645691caa..2f8ace10ddb 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/stretchr/testify/require" "gopkg.in/ini.v1" @@ -21,7 +22,6 @@ import ( "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/angulardetector" "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/registry" @@ -118,10 +118,12 @@ func TestIntegrationPluginManager(t *testing.T) { require.NoError(t, err) reg := registry.ProvideService() lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg}) + angularInspector, err := angularinspector.NewStaticInspector() + require.NoError(t, err) l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg), reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, statickey.New()), - angulardetector.NewDefaultPatternsListInspector()) + angularInspector) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 205325f1c53..ed69595d67c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -565,6 +565,13 @@ var ( Stage: FeatureStageExperimental, 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", Description: "Rewrites eligible loki range queries to instant queries", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index c77ce09c58f..60d826b5cd3 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -82,5 +82,6 @@ sqlDatasourceDatabaseSelection,preview,@grafana/grafana-bi-squad,false,false,fal cloudWatchLogsMonacoEditor,experimental,@grafana/aws-plugins,false,false,false,true exploreScrollableLogsContainer,experimental,@grafana/observability-logs,false,false,false,true 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 flameGraphV2,experimental,@grafana/observability-traces-and-profiling,false,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index a1c39802973..f818170de89 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -339,6 +339,10 @@ const ( // Enables writing multiple items from a single query within Recorded Queries FlagRecordedQueriesMulti = "recordedQueriesMulti" + // FlagPluginsDynamicAngularDetectionPatterns + // Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones + FlagPluginsDynamicAngularDetectionPatterns = "pluginsDynamicAngularDetectionPatterns" + // FlagAlertingLokiRangeToInstant // Rewrites eligible loki range queries to instant queries FlagAlertingLokiRangeToInstant = "alertingLokiRangeToInstant" diff --git a/pkg/services/pluginsintegration/angulardetector/gcom.go b/pkg/services/pluginsintegration/angulardetector/gcom.go new file mode 100644 index 00000000000..70ce04f988d --- /dev/null +++ b/pkg/services/pluginsintegration/angulardetector/gcom.go @@ -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 diff --git a/pkg/services/pluginsintegration/angulardetector/gcom_test.go b/pkg/services/pluginsintegration/angulardetector/gcom_test.go new file mode 100644 index 00000000000..5ad5c91b986 --- /dev/null +++ b/pkg/services/pluginsintegration/angulardetector/gcom_test.go @@ -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") + }) +} diff --git a/pkg/services/pluginsintegration/angularinspector/angularinspector.go b/pkg/services/pluginsintegration/angularinspector/angularinspector.go new file mode 100644 index 00000000000..bd945d8c500 --- /dev/null +++ b/pkg/services/pluginsintegration/angularinspector/angularinspector.go @@ -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 +} diff --git a/pkg/services/pluginsintegration/angularinspector/angularinspector_test.go b/pkg/services/pluginsintegration/angularinspector/angularinspector_test.go new file mode 100644 index 00000000000..7fb5f2f932e --- /dev/null +++ b/pkg/services/pluginsintegration/angularinspector/angularinspector_test.go @@ -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") + }) +} diff --git a/pkg/services/pluginsintegration/config/config.go b/pkg/services/pluginsintegration/config/config.go index 2e484c31104..6348266609a 100644 --- a/pkg/services/pluginsintegration/config/config.go +++ b/pkg/services/pluginsintegration/config/config.go @@ -42,6 +42,7 @@ func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg, fe tracingCfg, featuremgmt.ProvideToggles(features), grafanaCfg.AngularSupportEnabled, + grafanaCfg.GrafanaComURL, ), nil } diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 5d9b6d8e764..8c8cdcdcd03 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/filestore" "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/finder" "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/featuremgmt" "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/config" "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever" @@ -52,7 +53,8 @@ var WireSet = wire.NewSet( coreplugin.ProvideCoreRegistry, pluginscdn.ProvideService, assetpath.ProvideService, - angulardetector.ProvideService, + angularinspector.ProvideService, + wire.Bind(new(pAngularInspector.Inspector), new(*angularinspector.Service)), loader.ProvideService, wire.Bind(new(loader.Service), new(*loader.Loader)), wire.Bind(new(plugins.ErrorResolver), new(*loader.Loader)),