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.
|
// 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 |
|
||||||
} |
|
||||||
|
@ -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