mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
903af7e29c
commit
cca9d89733
@ -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.
|
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.
|
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) |
|
||||||
| `storage` | Configurable storage for dashboards, datasources, and resources |
|
| `storage` | Configurable storage for dashboards, datasources, and resources |
|
||||||
| `newTraceViewHeader` | Shows the new trace view header |
|
| `newTraceViewHeader` | Shows the new trace view header |
|
||||||
| `datasourceQueryMultiStatus` | Introduce HTTP 207 Multi Status for api/ds/query |
|
| `datasourceQueryMultiStatus` | Introduce HTTP 207 Multi Status for api/ds/query |
|
||||||
| `traceToMetrics` | Enable trace to metrics links |
|
| `traceToMetrics` | Enable trace to metrics links |
|
||||||
| `prometheusWideSeries` | Enable wide series responses in the Prometheus datasource |
|
| `prometheusWideSeries` | Enable wide series responses in the Prometheus datasource |
|
||||||
| `canvasPanelNesting` | Allow elements nesting |
|
| `canvasPanelNesting` | Allow elements nesting |
|
||||||
| `scenes` | Experimental framework to build interactive dashboards |
|
| `scenes` | Experimental framework to build interactive dashboards |
|
||||||
| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables |
|
| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables |
|
||||||
| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown |
|
| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown |
|
||||||
| `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift |
|
| `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift |
|
||||||
| `athenaAsyncQueryDataSupport` | Enable async query data support for Athena |
|
| `athenaAsyncQueryDataSupport` | Enable async query data support for Athena |
|
||||||
| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema |
|
| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema |
|
||||||
| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query |
|
| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query |
|
||||||
| `showTraceId` | Show trace ids for requests |
|
| `showTraceId` | Show trace ids for requests |
|
||||||
| `alertingBacktesting` | Rule backtesting API for alerting |
|
| `alertingBacktesting` | Rule backtesting API for alerting |
|
||||||
| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files |
|
| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files |
|
||||||
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals |
|
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals |
|
||||||
| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries |
|
| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries |
|
||||||
| `individualCookiePreferences` | Support overriding cookie preferences per user |
|
| `individualCookiePreferences` | Support overriding cookie preferences per user |
|
||||||
| `onlyExternalOrgRoleSync` | Prohibits a user from changing organization roles synced with external auth providers |
|
| `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 |
|
| `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 |
|
| `timeSeriesTable` | Enable time series table transformer & sparkline cell type |
|
||||||
| `prometheusResourceBrowserCache` | Displays browser caching options in Prometheus data source configuration |
|
| `prometheusResourceBrowserCache` | Displays browser caching options in Prometheus data source configuration |
|
||||||
| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy |
|
| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy |
|
||||||
| `clientTokenRotation` | Replaces the current in-request token rotation so that the client initiates the rotation |
|
| `clientTokenRotation` | Replaces the current in-request token rotation so that the client initiates the rotation |
|
||||||
| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. |
|
||||||
| `unifiedRequestLog` | Writes error logs to the request logger |
|
| `unifiedRequestLog` | Writes error logs to the request logger |
|
||||||
| `pyroscopeFlameGraph` | Changes flame graph to pyroscope one |
|
| `pyroscopeFlameGraph` | Changes flame graph to pyroscope one |
|
||||||
| `extraThemes` | Enables extra themes |
|
| `extraThemes` | Enables extra themes |
|
||||||
| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor |
|
| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor |
|
||||||
| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox |
|
| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox |
|
||||||
| `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 |
|
||||||
| `alertingLokiRangeToInstant` | Rewrites eligible loki range queries to instant queries |
|
| `pluginsDynamicAngularDetectionPatterns` | Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones |
|
||||||
| `flameGraphV2` | New version of flame graph with new features |
|
| `alertingLokiRangeToInstant` | Rewrites eligible loki range queries to instant queries |
|
||||||
|
| `flameGraphV2` | New version of flame graph with new features |
|
||||||
|
|
||||||
## Development feature toggles
|
## Development feature toggles
|
||||||
|
|
||||||
|
@ -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 = ®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)
|
|
||||||
}
|
|
@ -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")},
|
|
||||||
|
|
||||||
®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
|
|
||||||
}
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
157
pkg/services/pluginsintegration/angulardetector/gcom.go
Normal file
157
pkg/services/pluginsintegration/angulardetector/gcom.go
Normal file
@ -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
|
144
pkg/services/pluginsintegration/angulardetector/gcom_test.go
Normal file
144
pkg/services/pluginsintegration/angulardetector/gcom_test.go
Normal file
@ -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…
Reference in New Issue
Block a user