mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Angular deprecation: Detect Angular plugins and expose in API (#66824)
* Plugins: Angular deprecation: Detect Angular plugins and expose in API * Plugins: Angular detector: Close module.js * Plugins: Angular detector: consistent error messages * Plugins: Angular detector: Add test for missing module.js * Plugins: Angular detector: Fix integration tests * Plugins: Angular detector: Changed Angular detection patterns * Moved inMemoryFS to test_utils.go * Add different angular detectors * Plugins: Update plugins/data/expectedListResp.json * Plugins: Rename angular property to angularDetected * Plugins: Rename angular to angularDetected in Plugin and PluginDTO * Plugins: Add angularDetected to datasources, apps and plugins frontendsettings * Plugins: Add test for AngularDetected frontend settings
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
package angulardetector
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"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)
|
||||
}
|
||||
|
||||
// angularDetectors contains all the detectors to detect Angular plugins.
|
||||
// They are executed in the specified order.
|
||||
var angularDetectors = []detector{
|
||||
&containsBytesDetector{pattern: []byte("PanelCtrl")},
|
||||
&containsBytesDetector{pattern: []byte("QueryCtrl")},
|
||||
&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(`['"](app/core/utils/promiseToDigest)|(app/plugins/.*?)|(app/core/core_module)['"]`)},
|
||||
®exDetector{regex: regexp.MustCompile(`from\s+['"]grafana\/app\/`)},
|
||||
®exDetector{regex: regexp.MustCompile(`System\.register\(`)},
|
||||
}
|
||||
|
||||
// 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.
|
||||
func 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 angularDetectors {
|
||||
if d.Detect(b) {
|
||||
isAngular = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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`),
|
||||
} {
|
||||
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,
|
||||
})
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
isAngular, err := 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 := Inspect(p)
|
||||
require.ErrorIs(t, err, plugins.ErrFileNotExist)
|
||||
})
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/angulardetector"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
|
||||
@@ -153,6 +154,15 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun
|
||||
}
|
||||
}
|
||||
|
||||
// Detect angular for external plugins
|
||||
if plugin.IsExternalPlugin() {
|
||||
var err error
|
||||
plugin.AngularDetected, err = angulardetector.Inspect(plugin)
|
||||
if err != nil {
|
||||
l.log.Warn("could not inspect plugin for angular", "pluginID", plugin.ID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if plugin.IsApp() {
|
||||
setDefaultNavURL(plugin)
|
||||
}
|
||||
|
||||
@@ -210,18 +210,19 @@ type PluginMetaDTO struct {
|
||||
}
|
||||
|
||||
type DataSourceDTO struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
UID string `json:"uid,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
PluginMeta *PluginMetaDTO `json:"meta"`
|
||||
URL string `json:"url,omitempty"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Access string `json:"access,omitempty"`
|
||||
Preload bool `json:"preload"`
|
||||
Module string `json:"module,omitempty"`
|
||||
JSONData map[string]interface{} `json:"jsonData"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
ID int64 `json:"id,omitempty"`
|
||||
UID string `json:"uid,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
PluginMeta *PluginMetaDTO `json:"meta"`
|
||||
URL string `json:"url,omitempty"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Access string `json:"access,omitempty"`
|
||||
Preload bool `json:"preload"`
|
||||
Module string `json:"module,omitempty"`
|
||||
JSONData map[string]interface{} `json:"jsonData"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
AngularDetected bool `json:"angularDetected"`
|
||||
|
||||
BasicAuth string `json:"basicAuth,omitempty"`
|
||||
WithCredentials bool `json:"withCredentials,omitempty"`
|
||||
@@ -241,23 +242,25 @@ type DataSourceDTO struct {
|
||||
}
|
||||
|
||||
type PanelDTO struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Info Info `json:"info"`
|
||||
HideFromList bool `json:"hideFromList"`
|
||||
Sort int `json:"sort"`
|
||||
SkipDataQuery bool `json:"skipDataQuery"`
|
||||
ReleaseState string `json:"state"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Signature string `json:"signature"`
|
||||
Module string `json:"module"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Info Info `json:"info"`
|
||||
HideFromList bool `json:"hideFromList"`
|
||||
Sort int `json:"sort"`
|
||||
SkipDataQuery bool `json:"skipDataQuery"`
|
||||
ReleaseState string `json:"state"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Signature string `json:"signature"`
|
||||
Module string `json:"module"`
|
||||
AngularDetected bool `json:"angularDetected"`
|
||||
}
|
||||
|
||||
type AppDTO struct {
|
||||
ID string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Version string `json:"version"`
|
||||
Preload bool `json:"preload"`
|
||||
ID string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Version string `json:"version"`
|
||||
Preload bool `json:"preload"`
|
||||
AngularDetected bool `json:"angularDetected"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -51,6 +51,8 @@ type Plugin struct {
|
||||
Module string
|
||||
BaseURL string
|
||||
|
||||
AngularDetected bool
|
||||
|
||||
Renderer pluginextensionv2.RendererPlugin
|
||||
SecretsManager secretsmanagerplugin.SecretsManagerPlugin
|
||||
client backendplugin.Plugin
|
||||
@@ -80,6 +82,8 @@ type PluginDTO struct {
|
||||
// SystemJS fields
|
||||
Module string
|
||||
BaseURL string
|
||||
|
||||
AngularDetected bool
|
||||
}
|
||||
|
||||
func (p PluginDTO) SupportsStreaming() bool {
|
||||
@@ -424,6 +428,7 @@ func (p *Plugin) ToDTO() PluginDTO {
|
||||
SignatureError: p.SignatureError,
|
||||
Module: p.Module,
|
||||
BaseURL: p.BaseURL,
|
||||
AngularDetected: p.AngularDetected,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user