mirror of https://github.com/grafana/grafana
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 logpull/69764/head^2
parent
903af7e29c
commit
cca9d89733
@ -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.
|
||||
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 |
||||
}, |
||||
} |
||||
@ -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 |
||||
} |
||||
|
@ -0,0 +1,157 @@ |
||||
package angulardetector |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
"regexp" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" |
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/log" |
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector" |
||||
) |
||||
|
||||
const ( |
||||
// gcomAngularPatternsPath is the relative path to the GCOM API handler that returns angular detection patterns.
|
||||
gcomAngularPatternsPath = "/api/plugins/angular_patterns" |
||||
) |
||||
|
||||
var _ angulardetector.DetectorsProvider = &GCOMDetectorsProvider{} |
||||
|
||||
// GCOMDetectorsProvider is a DetectorsProvider which fetches patterns from GCOM.
|
||||
type GCOMDetectorsProvider struct { |
||||
log log.Logger |
||||
|
||||
httpClient *http.Client |
||||
|
||||
baseURL string |
||||
} |
||||
|
||||
// NewGCOMDetectorsProvider returns a new GCOMDetectorsProvider.
|
||||
// baseURL is the GCOM base url, without /api and without a trailing slash (e.g.: https://grafana.com)
|
||||
func NewGCOMDetectorsProvider(baseURL string) (angulardetector.DetectorsProvider, error) { |
||||
cl, err := httpclient.New() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("httpclient new: %w", err) |
||||
} |
||||
return &GCOMDetectorsProvider{ |
||||
log: log.New("plugins.angulardetector.gcom"), |
||||
baseURL: baseURL, |
||||
httpClient: cl, |
||||
}, nil |
||||
} |
||||
|
||||
// ProvideDetectors gets the dynamic angular detectors from the remote source.
|
||||
// If an error occurs, the function fails silently by logging an error, and it returns nil.
|
||||
func (p *GCOMDetectorsProvider) ProvideDetectors(ctx context.Context) []angulardetector.AngularDetector { |
||||
patterns, err := p.fetch(ctx) |
||||
if err != nil { |
||||
p.log.Warn("Could not fetch remote angular patterns", "error", err) |
||||
return nil |
||||
} |
||||
detectors, err := p.patternsToDetectors(patterns) |
||||
if err != nil { |
||||
p.log.Warn("Could not convert angular patterns to angularDetectors", "error", err) |
||||
return nil |
||||
} |
||||
return detectors |
||||
} |
||||
|
||||
// fetch fetches the angular patterns from GCOM and returns them as gcomPatterns.
|
||||
// Call angularDetectors() on the returned value to get the corresponding angular detectors.
|
||||
func (p *GCOMDetectorsProvider) fetch(ctx context.Context) (gcomPatterns, error) { |
||||
st := time.Now() |
||||
|
||||
reqURL, err := url.JoinPath(p.baseURL, gcomAngularPatternsPath) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("url joinpath: %w", err) |
||||
} |
||||
|
||||
p.log.Debug("Fetching dynamic angular detection patterns", "url", reqURL) |
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("new request with context: %w", err) |
||||
} |
||||
resp, err := p.httpClient.Do(req) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("http do: %w", err) |
||||
} |
||||
defer func() { |
||||
if closeErr := resp.Body.Close(); closeErr != nil { |
||||
p.log.Error("response body close error", "error", err) |
||||
} |
||||
}() |
||||
var out gcomPatterns |
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { |
||||
return nil, fmt.Errorf("json decode: %w", err) |
||||
} |
||||
p.log.Debug("Fetched dynamic angular detection patterns", "patterns", len(out), "duration", time.Since(st)) |
||||
return out, nil |
||||
} |
||||
|
||||
// patternsToDetectors converts a slice of gcomPattern into a slice of angulardetector.AngularDetector, by calling
|
||||
// angularDetector() on each gcomPattern.
|
||||
func (p *GCOMDetectorsProvider) patternsToDetectors(patterns gcomPatterns) ([]angulardetector.AngularDetector, error) { |
||||
var finalErr error |
||||
detectors := make([]angulardetector.AngularDetector, 0, len(patterns)) |
||||
for _, pattern := range patterns { |
||||
d, err := pattern.angularDetector() |
||||
if err != nil { |
||||
// Fail silently in case of an errUnknownPatternType.
|
||||
// This allows us to introduce new pattern types without breaking old Grafana versions
|
||||
if errors.Is(err, errUnknownPatternType) { |
||||
p.log.Debug("Unknown angular pattern", "name", pattern.Name, "type", pattern.Type, "error", err) |
||||
continue |
||||
} |
||||
// Other error, do not ignore it
|
||||
finalErr = errors.Join(finalErr, err) |
||||
} |
||||
detectors = append(detectors, d) |
||||
} |
||||
if finalErr != nil { |
||||
return nil, finalErr |
||||
} |
||||
return detectors, nil |
||||
} |
||||
|
||||
// gcomPatternType is a pattern type returned by the GCOM API.
|
||||
type gcomPatternType string |
||||
|
||||
const ( |
||||
gcomPatternTypeContains gcomPatternType = "contains" |
||||
gcomPatternTypeRegex gcomPatternType = "regex" |
||||
) |
||||
|
||||
// errUnknownPatternType is returned when a pattern type is not known.
|
||||
var errUnknownPatternType = errors.New("unknown pattern type") |
||||
|
||||
// gcomPattern is an Angular detection pattern returned by the GCOM API.
|
||||
type gcomPattern struct { |
||||
Name string |
||||
Pattern string |
||||
Type gcomPatternType |
||||
} |
||||
|
||||
// angularDetector converts a gcomPattern into an AngularDetector, based on its Type.
|
||||
// If a pattern type is unknown, it returns an error wrapping errUnknownPatternType.
|
||||
func (p *gcomPattern) angularDetector() (angulardetector.AngularDetector, error) { |
||||
switch p.Type { |
||||
case gcomPatternTypeContains: |
||||
return &angulardetector.ContainsBytesDetector{Pattern: []byte(p.Pattern)}, nil |
||||
case gcomPatternTypeRegex: |
||||
re, err := regexp.Compile(p.Pattern) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("%q regexp compile: %w", p.Pattern, err) |
||||
} |
||||
return &angulardetector.RegexDetector{Regex: re}, nil |
||||
} |
||||
return nil, fmt.Errorf("%q: %w", p.Type, errUnknownPatternType) |
||||
} |
||||
|
||||
// gcomPatterns is a slice of gcomPattern s.
|
||||
type gcomPatterns []gcomPattern |
||||
@ -0,0 +1,144 @@ |
||||
package angulardetector |
||||
|
||||
import ( |
||||
"context" |
||||
"net/http" |
||||
"net/http/httptest" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector" |
||||
) |
||||
|
||||
var mockGCOMResponse = []byte(`[{ |
||||
"name": "PanelCtrl", |
||||
"type": "contains", |
||||
"pattern": "PanelCtrl" |
||||
}, |
||||
{ |
||||
"name": "QueryCtrl", |
||||
"type": "regex", |
||||
"pattern": "[\"']QueryCtrl[\"']" |
||||
}]`) |
||||
|
||||
func mockGCOMHTTPHandlerFunc(writer http.ResponseWriter, request *http.Request) { |
||||
if request.URL.Path != "/api/plugins/angular_patterns" { |
||||
writer.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
_, _ = writer.Write(mockGCOMResponse) |
||||
} |
||||
|
||||
func checkMockGCOMResponse(t *testing.T, detectors []angulardetector.AngularDetector) { |
||||
require.Len(t, detectors, 2) |
||||
d, ok := detectors[0].(*angulardetector.ContainsBytesDetector) |
||||
require.True(t, ok) |
||||
require.Equal(t, []byte(`PanelCtrl`), d.Pattern) |
||||
rd, ok := detectors[1].(*angulardetector.RegexDetector) |
||||
require.True(t, ok) |
||||
require.Equal(t, `["']QueryCtrl["']`, rd.Regex.String()) |
||||
} |
||||
|
||||
type gcomScenario struct { |
||||
gcomHTTPHandlerFunc http.HandlerFunc |
||||
gcomHTTPCalls int |
||||
} |
||||
|
||||
func (s *gcomScenario) newHTTPTestServer() *httptest.Server { |
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||
s.gcomHTTPCalls++ |
||||
s.gcomHTTPHandlerFunc(w, r) |
||||
})) |
||||
} |
||||
|
||||
func newDefaultGCOMScenario() *gcomScenario { |
||||
return &gcomScenario{gcomHTTPHandlerFunc: mockGCOMHTTPHandlerFunc} |
||||
} |
||||
|
||||
func TestGCOMDetectorsProvider(t *testing.T) { |
||||
t.Run("returns value returned from gcom api", func(t *testing.T) { |
||||
scenario := newDefaultGCOMScenario() |
||||
srv := scenario.newHTTPTestServer() |
||||
t.Cleanup(srv.Close) |
||||
gcomProvider, err := NewGCOMDetectorsProvider(srv.URL) |
||||
require.NoError(t, err) |
||||
detectors := gcomProvider.ProvideDetectors(context.Background()) |
||||
require.Equal(t, 1, scenario.gcomHTTPCalls, "gcom api should be called") |
||||
checkMockGCOMResponse(t, detectors) |
||||
}) |
||||
|
||||
t.Run("error handling", func(t *testing.T) { |
||||
for _, tc := range []struct { |
||||
*gcomScenario |
||||
name string |
||||
}{ |
||||
{name: "http error 500", gcomScenario: &gcomScenario{ |
||||
gcomHTTPHandlerFunc: func(writer http.ResponseWriter, request *http.Request) { |
||||
writer.WriteHeader(http.StatusInternalServerError) |
||||
}, |
||||
}}, |
||||
{name: "invalid json", gcomScenario: &gcomScenario{ |
||||
gcomHTTPHandlerFunc: func(writer http.ResponseWriter, request *http.Request) { |
||||
_, _ = writer.Write([]byte(`not json`)) |
||||
}, |
||||
}}, |
||||
{name: "invalid regex", gcomScenario: &gcomScenario{ |
||||
gcomHTTPHandlerFunc: func(writer http.ResponseWriter, request *http.Request) { |
||||
_, _ = writer.Write([]byte(`[{"name": "test", "type": "regex", "pattern": "((("}]`)) |
||||
}, |
||||
}}, |
||||
} { |
||||
t.Run(tc.name, func(t *testing.T) { |
||||
srv := tc.newHTTPTestServer() |
||||
t.Cleanup(srv.Close) |
||||
gcomProvider, err := NewGCOMDetectorsProvider(srv.URL) |
||||
require.NoError(t, err) |
||||
detectors := gcomProvider.ProvideDetectors(context.Background()) |
||||
require.Equal(t, 1, tc.gcomHTTPCalls, "gcom should be called") |
||||
require.Empty(t, detectors, "returned AngularDetectors should be empty") |
||||
}) |
||||
} |
||||
}) |
||||
|
||||
t.Run("handles gcom timeout", func(t *testing.T) { |
||||
gcomScenario := &gcomScenario{ |
||||
gcomHTTPHandlerFunc: func(writer http.ResponseWriter, request *http.Request) { |
||||
time.Sleep(time.Second * 1) |
||||
_, _ = writer.Write([]byte(`[{"name": "test", "type": "regex", "pattern": "((("}]`)) |
||||
}, |
||||
} |
||||
srv := gcomScenario.newHTTPTestServer() |
||||
t.Cleanup(srv.Close) |
||||
gcomProvider, err := NewGCOMDetectorsProvider(srv.URL) |
||||
require.NoError(t, err) |
||||
// Expired context
|
||||
ctx, canc := context.WithTimeout(context.Background(), time.Second*-1) |
||||
defer canc() |
||||
detectors := gcomProvider.ProvideDetectors(ctx) |
||||
require.Zero(t, gcomScenario.gcomHTTPCalls, "gcom should be not called due to request timing out") |
||||
require.Empty(t, detectors, "returned AngularDetectors should be empty") |
||||
}) |
||||
|
||||
t.Run("unknown pattern types do not break decoding", func(t *testing.T) { |
||||
// Tests that we can introduce new pattern types in the future without breaking old Grafana versions.
|
||||
|
||||
scenario := gcomScenario{gcomHTTPHandlerFunc: func(writer http.ResponseWriter, request *http.Request) { |
||||
_, _ = writer.Write([]byte(`[ |
||||
{"name": "PanelCtrl", "type": "contains", "pattern": "PanelCtrl"}, |
||||
{"name": "Another", "type": "unknown", "pattern": "PanelCtrl"} |
||||
]`)) |
||||
}} |
||||
srv := scenario.newHTTPTestServer() |
||||
t.Cleanup(srv.Close) |
||||
gcomProvider, err := NewGCOMDetectorsProvider(srv.URL) |
||||
require.NoError(t, err) |
||||
detectors := gcomProvider.ProvideDetectors(context.Background()) |
||||
require.Equal(t, 1, scenario.gcomHTTPCalls, "gcom should be called") |
||||
require.Len(t, detectors, 1, "should have decoded only 1 AngularDetector") |
||||
d, ok := detectors[0].(*angulardetector.ContainsBytesDetector) |
||||
require.True(t, ok, "decoded pattern should be of the correct type") |
||||
require.Equal(t, []byte("PanelCtrl"), d.Pattern, "decoded value for known pattern should be correct") |
||||
}) |
||||
} |
||||
@ -0,0 +1,45 @@ |
||||
package angularinspector |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/config" |
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector" |
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" |
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
pAngularDetector "github.com/grafana/grafana/pkg/services/pluginsintegration/angulardetector" |
||||
) |
||||
|
||||
type Service struct { |
||||
angularinspector.Inspector |
||||
} |
||||
|
||||
// newDynamicInspector returns the default dynamic Inspector, which is a PatternsListInspector that will:
|
||||
// 1. Try to get the Angular detectors from GCOM
|
||||
// 2. If it fails, it will use the static (hardcoded) detections provided by defaultDetectors.
|
||||
func newDynamicInspector(cfg *config.Cfg) (angularinspector.Inspector, error) { |
||||
dynamicProvider, err := pAngularDetector.NewGCOMDetectorsProvider(cfg.GrafanaComURL) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("NewGCOMDetectorsProvider: %w", err) |
||||
} |
||||
return &angularinspector.PatternsListInspector{ |
||||
DetectorsProvider: angulardetector.SequenceDetectorsProvider{ |
||||
dynamicProvider, |
||||
angularinspector.NewDefaultStaticDetectorsProvider(), |
||||
}, |
||||
}, nil |
||||
} |
||||
|
||||
func ProvideService(cfg *config.Cfg) (*Service, error) { |
||||
var underlying angularinspector.Inspector |
||||
var err error |
||||
if cfg.Features != nil && cfg.Features.IsEnabled(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns) { |
||||
underlying, err = newDynamicInspector(cfg) |
||||
} else { |
||||
underlying, err = angularinspector.NewStaticInspector() |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &Service{underlying}, nil |
||||
} |
||||
@ -0,0 +1,42 @@ |
||||
package angularinspector |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/config" |
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector" |
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" |
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
pAngularDetector "github.com/grafana/grafana/pkg/services/pluginsintegration/angulardetector" |
||||
) |
||||
|
||||
func TestProvideService(t *testing.T) { |
||||
t.Run("uses hardcoded inspector if feature flag is not present", func(t *testing.T) { |
||||
inspector, err := ProvideService(&config.Cfg{ |
||||
Features: featuremgmt.WithFeatures(), |
||||
}) |
||||
require.NoError(t, err) |
||||
require.IsType(t, inspector.Inspector, &angularinspector.PatternsListInspector{}) |
||||
patternsListInspector := inspector.Inspector.(*angularinspector.PatternsListInspector) |
||||
detectors := patternsListInspector.DetectorsProvider.ProvideDetectors(context.Background()) |
||||
require.NotEmpty(t, detectors, "provided detectors should not be empty") |
||||
}) |
||||
|
||||
t.Run("uses dynamic inspector with hardcoded fallback if feature flag is present", func(t *testing.T) { |
||||
inspector, err := ProvideService(&config.Cfg{ |
||||
Features: featuremgmt.WithFeatures(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns), |
||||
}) |
||||
require.NoError(t, err) |
||||
require.IsType(t, inspector.Inspector, &angularinspector.PatternsListInspector{}) |
||||
require.IsType(t, inspector.Inspector.(*angularinspector.PatternsListInspector).DetectorsProvider, angulardetector.SequenceDetectorsProvider{}) |
||||
seq := inspector.Inspector.(*angularinspector.PatternsListInspector).DetectorsProvider.(angulardetector.SequenceDetectorsProvider) |
||||
require.Len(t, seq, 2, "should return the correct number of providers") |
||||
require.IsType(t, seq[0], &pAngularDetector.GCOMDetectorsProvider{}, "first AngularDetector provided should be gcom") |
||||
require.IsType(t, seq[1], &angulardetector.StaticDetectorsProvider{}, "second AngularDetector provided should be static") |
||||
staticDetectors := seq[1].ProvideDetectors(context.Background()) |
||||
require.NotEmpty(t, staticDetectors, "provided static detectors should not be empty") |
||||
}) |
||||
} |
||||
Loading…
Reference in new issue