Plugins: Angular detector: Remote patterns fetching (#69843)
* Plugins: Angular detector: Remote patterns fetching * Renamed PatternType to GCOMPatternType * Renamed files * Renamed more files * Moved files again * Add type checks, unexport GCOM structs * Cache failures, update log messages, fix GCOM URL * Fail silently for unknown pattern types, update docstrings * Fix tests * Rename gcomPattern.Value to gcomPattern.Pattern * Refactoring * Add FlagPluginsRemoteAngularDetectionPatterns feature flag * Fix tests * Re-generate feature flags * Add TestProvideInspector, renamed TestDefaultStaticDetectorsInspector * Add TestProvideInspector * Add TestContainsBytesDetector and TestRegexDetector * Renamed getter to provider * More tests * TestStaticDetectorsProvider, TestSequenceDetectorsProvider * GCOM tests * Lint * Made detector.detect unexported, updated docstrings * Allow changing grafana.com URL * Fix API path, add more logs * Update tryUpdateRemoteDetectors docstring * Use angulardetector http client * Return false, nil if module.js does not exist * Chore: Split angualrdetector into angularinspector and angulardetector packages Moved files around, changed references and fixed tests: - Split the old angulardetector package into angular/angulardetector and angular/angularinspector - angulardetector provides the detection structs/interfaces (Detector, DetectorsProvider...) - angularinspector provides the actual angular detection service used directly in pluginsintegration - Exported most of the stuff that was private and now put into angulardetector, as it is not required by angularinspector * Renamed detector.go -> angulardetector.go and inspector.go -> angularinspector.go Forgot to rename those two files to match the package's names * Renamed angularinspector.ProvideInspector to angularinspector.ProvideService * Renamed "harcoded" to "static" and "remote" to "dynamic" from PR review, matches the same naming schema used for signing keys fetching * Fix merge conflict on updated angular patterns * Removed GCOM cache * Renamed Detect to DetectAngular and Detector to AngularDetector * Fix call to NewGCOMDetectorsProvider in newDynamicInspector * Removed unused test function newError500GCOMScenario * Added angularinspector service definition in pluginsintegration * Moved dynamic inspector into pluginsintegration * Move gcom angulardetectorsprovider into pluginsintegration * Log errUnknownPatternType at debug level * re-generate feature flags * fix error log
This commit is contained in:
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package angularinspector
|
||||
|
||||
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(ctx context.Context, p *plugins.Plugin) (bool, error)
|
||||
}
|
||||
|
||||
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(_ context.Context, _ *plugins.Plugin) (bool, error) {
|
||||
return true, nil
|
||||
},
|
||||
}
|
||||
|
||||
// NeverAngularFakeInspector is an inspector that always returns `false, nil`
|
||||
NeverAngularFakeInspector = &FakeInspector{
|
||||
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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user