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
This commit is contained in:
Giuseppe Guerra
2023-06-19 17:33:54 +02:00
parent 82b190944b
commit e15793a89b
17 changed files with 346 additions and 352 deletions
@@ -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)
})
}
@@ -0,0 +1,108 @@
package angularinspector
import (
"context"
"errors"
"fmt"
"io"
"regexp"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
// 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.Detect(b) {
isAngular = true
break
}
}
return
}
// defaultDetectors contains all the detectors to Detect Angular plugins.
// They are executed in the specified order.
var defaultDetectors = []angulardetector.Detector{
&angulardetector.ContainsBytesDetector{Pattern: []byte("PanelCtrl")},
&angulardetector.ContainsBytesDetector{Pattern: []byte("QueryCtrl")},
&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(`['"](app/core/utils/promiseToDigest)|(app/plugins/.*?)|(app/core/core_module)['"]`)},
&angulardetector.RegexDetector{Regex: regexp.MustCompile(`from\s+['"]grafana\/app\/`)},
&angulardetector.RegexDetector{Regex: regexp.MustCompile(`System\.register\(`)},
}
// newDefaultStaticDetectorsProvider returns a new StaticDetectorsProvider with the default (hardcoded) angular
// detection patterns (defaultDetectors)
func newDefaultStaticDetectorsProvider() angulardetector.DetectorsProvider {
return &angulardetector.StaticDetectorsProvider{Detectors: defaultDetectors}
}
// newRemoteInspector returns the default remote Inspector, which is a PatternsListInspector that will:
// 1. Try to get the Angular detectors from GCOM
// 2. If it fails, it will use the hardcoded detections provided by defaultDetectors.
func newRemoteInspector(cfg *config.Cfg) (Inspector, error) {
remoteProvider, err := angulardetector.NewGCOMDetectorsProvider(
cfg.GrafanaComURL,
angulardetector.DefaultGCOMDetectorsProviderTTL,
)
if err != nil {
return nil, fmt.Errorf("newGCOMDetectorsProvider: %w", err)
}
return &PatternsListInspector{
DetectorsProvider: angulardetector.SequenceDetectorsProvider{
remoteProvider,
newDefaultStaticDetectorsProvider(),
},
}, nil
}
// newHardcodedInspector returns the default Inspector, which is a PatternsListInspector that only uses the
// hardcoded (static) angular detection patterns.
func newHardcodedInspector() (Inspector, error) {
return &PatternsListInspector{DetectorsProvider: newDefaultStaticDetectorsProvider()}, nil
}
func ProvideInspector(cfg *config.Cfg) (Inspector, error) {
if cfg.Features != nil && cfg.Features.IsEnabled(featuremgmt.FlagPluginsRemoteAngularDetectionPatterns) {
return newRemoteInspector(cfg)
}
return newHardcodedInspector()
}
@@ -0,0 +1,169 @@
package angularinspector
import (
"context"
"strconv"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
type fakeDetector struct {
calls int
returns bool
}
func (d *fakeDetector) Detect(_ []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.Detector, 0, len(tc.fakeDetectors))
for _, d := range tc.fakeDetectors {
detectors = append(detectors, angulardetector.Detector(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`),
} {
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
tcs = append(tcs, tc{
name: "not angular",
plugin: &plugins.Plugin{
FS: plugins.NewInMemoryFS(map[string][]byte{
"module.js": []byte(`import { PanelPlugin } from '@grafana/data'`),
}),
},
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)
})
}
func TestProvideInspector(t *testing.T) {
t.Run("uses hardcoded inspector if feature flag is not present", func(t *testing.T) {
inspector, err := ProvideInspector(&config.Cfg{
Features: featuremgmt.WithFeatures(),
})
require.NoError(t, err)
require.IsType(t, inspector, &PatternsListInspector{})
patternsListInspector := inspector.(*PatternsListInspector)
detectors := patternsListInspector.DetectorsProvider.ProvideDetectors(context.Background())
require.NotEmpty(t, detectors, "provided detectors should not be empty")
require.Equal(t, defaultDetectors, detectors, "provided detectors should be the hardcoded ones")
})
t.Run("uses remote inspector with hardcoded fallback if feature flag is present", func(t *testing.T) {
inspector, err := ProvideInspector(&config.Cfg{
Features: featuremgmt.WithFeatures(featuremgmt.FlagPluginsRemoteAngularDetectionPatterns),
})
require.NoError(t, err)
require.IsType(t, inspector, &PatternsListInspector{})
require.IsType(t, inspector.(*PatternsListInspector).DetectorsProvider, angulardetector.SequenceDetectorsProvider{})
seq := inspector.(*PatternsListInspector).DetectorsProvider.(angulardetector.SequenceDetectorsProvider)
require.Len(t, seq, 2, "should return the correct number of providers")
require.IsType(t, seq[0], &angulardetector.GCOMDetectorsProvider{}, "first Detector provided should be gcom")
require.IsType(t, seq[1], &angulardetector.StaticDetectorsProvider{}, "second Detector provided should be static")
staticDetectors := seq[1].ProvideDetectors(context.Background())
require.NotEmpty(t, staticDetectors, "provided static detectors should not be empty")
require.Equal(t, defaultDetectors, staticDetectors, "should provide hardcoded detectors as fallback")
})
}