Plugins: Remove pkg/infra/fs, pkg/infra/tracing and pkg/infra/process dependencies from pkg/plugins (#115798)

* remove dependency on packages

* update tests

* trigger
This commit is contained in:
Will Browne
2026-01-05 11:12:31 +00:00
committed by GitHub
parent 4e3039e4bd
commit 70b1053ad1
33 changed files with 131 additions and 134 deletions
@@ -1,20 +1,20 @@
package coreplugin_test
package coreplugin
import (
"context"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
"github.com/stretchr/testify/require"
)
func TestCorePlugin(t *testing.T) {
t.Run("New core plugin with empty opts should return expected values", func(t *testing.T) {
factory := coreplugin.New(backend.ServeOpts{})
factory := New(backend.ServeOpts{})
p, err := factory("plugin", log.New("test"), pluginfakes.InitializeNoopTracerForTest(), nil)
require.NoError(t, err)
require.NotNil(t, p)
@@ -36,7 +36,7 @@ func TestCorePlugin(t *testing.T) {
t.Run("New core plugin with handlers set in opts should return expected values", func(t *testing.T) {
checkHealthCalled := false
callResourceCalled := false
factory := coreplugin.New(backend.ServeOpts{
factory := New(backend.ServeOpts{
CheckHealthHandler: backend.CheckHealthHandlerFunc(func(ctx context.Context,
req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
checkHealthCalled = true
@@ -5,18 +5,16 @@ import (
"errors"
"fmt"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
sdklog "github.com/grafana/grafana-plugin-sdk-go/backend/log"
sdktracing "github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
cloudmonitoring "github.com/grafana/grafana/pkg/tsdb/cloud-monitoring"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch"
@@ -204,7 +202,7 @@ var ErrCorePluginNotFound = errors.New("core plugin not found")
// NewPlugin factory for creating and initializing a single core plugin.
// Note: cfg only needed for mssql connection pooling defaults.
func NewPlugin(pluginID string, cfg *setting.Cfg, httpClientProvider *httpclient.Provider, tracer trace.Tracer, features featuremgmt.FeatureToggles) (*plugins.Plugin, error) {
func NewPlugin(pluginID string, httpClientProvider *httpclient.Provider, tracer trace.Tracer) (*plugins.Plugin, error) {
jsonData := plugins.JSONData{
ID: pluginID,
AliasIDs: []string{},
@@ -241,7 +239,7 @@ func NewPlugin(pluginID string, cfg *setting.Cfg, httpClientProvider *httpclient
case MySQL:
svc = mysql.ProvideService()
case MSSQL:
svc = mssql.ProvideService(cfg)
svc = mssql.ProvideService()
case Pyroscope:
svc = pyroscope.ProvideService(httpClientProvider)
case Parca:
@@ -4,11 +4,10 @@ import (
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func TestNewPlugin(t *testing.T) {
@@ -46,7 +45,7 @@ func TestNewPlugin(t *testing.T) {
tc.ExpectedID = tc.ID
}
p, err := NewPlugin(tc.ID, setting.NewCfg(), httpclient.NewProvider(), tracing.NoopTracer(), featuremgmt.WithFeatures())
p, err := NewPlugin(tc.ID, httpclient.NewProvider(), tracing.NoopTracer())
if tc.ExpectedNotFoundErr {
require.ErrorIs(t, err, ErrCorePluginNotFound)
require.Nil(t, p)
+5 -6
View File
@@ -1,9 +1,5 @@
package config
import (
"github.com/grafana/grafana/pkg/setting"
)
// PluginManagementCfg is the configuration for the plugin management system.
// It includes settings which are used to configure different components of plugin management.
type PluginManagementCfg struct {
@@ -11,7 +7,7 @@ type PluginManagementCfg struct {
PluginsPath string
PluginSettings setting.PluginSettings
PluginSettings PluginSettings
PluginsAllowUnsigned []string
DisablePlugins []string
ForwardHostEnvVars []string
@@ -35,8 +31,11 @@ type Features struct {
TempoAlertingEnabled bool
}
// PluginSettings maps plugin id to map of key/value settings.
type PluginSettings map[string]map[string]string
// NewPluginManagementCfg returns a new PluginManagementCfg.
func NewPluginManagementCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string,
func NewPluginManagementCfg(devMode bool, pluginsPath string, pluginSettings PluginSettings, pluginsAllowUnsigned []string,
pluginsCDNURLTemplate string, appURL string, features Features,
grafanaComAPIURL string, disablePlugins []string, forwardHostEnvVars []string, grafanaComAPIToken string,
) *PluginManagementCfg {
+167
View File
@@ -0,0 +1,167 @@
package plugins
import (
"errors"
"fmt"
"os"
"path/filepath"
)
// ErrWalkSkipDir is the Error returned when we want to skip descending into a directory
var ErrWalkSkipDir = errors.New("skip this directory")
// WalkFunc is a callback function called for each path as a directory is walked
// If resolvedPath != "", then we are following symbolic links.
type WalkFunc func(resolvedPath string, info os.FileInfo, err error) error
type walker struct {
rootDir string
}
// newWalker creates a new walker
func newWalker(rootDir string) *walker {
return &walker{rootDir: rootDir}
}
// Walk walks a path, optionally following symbolic links, and for each path,
// it calls the walkFn passed.
//
// It is similar to filepath.Walk, except that it supports symbolic links and
// can detect infinite loops while following symlinks.
// It solves the issue where your WalkFunc needs a path relative to the symbolic link
// (resolving links within walkfunc loses the path to the symbolic link for each traversal).
func Walk(path string, followSymlinks bool, detectSymlinkInfiniteLoop bool, walkFn WalkFunc) error {
info, err := os.Lstat(path)
if err != nil {
return err
}
var symlinkPathsFollowed map[string]bool
var resolvedPath string
if followSymlinks {
resolvedPath = path
if detectSymlinkInfiniteLoop {
symlinkPathsFollowed = make(map[string]bool, 8)
}
}
return newWalker(path).walk(path, info, resolvedPath, symlinkPathsFollowed, walkFn)
}
// walk walks the path. It is a helper/sibling function to Walk.
// It takes a resolvedPath into consideration. This way, paths being walked are
// always relative to the path argument, even if symbolic links were resolved).
//
// If resolvedPath is "", then we are not following symbolic links.
// If symlinkPathsFollowed is not nil, then we need to detect infinite loop.
func (w *walker) walk(path string, info os.FileInfo, resolvedPath string, symlinkPathsFollowed map[string]bool, walkFn WalkFunc) error {
if info == nil {
return errors.New("walk: Nil FileInfo passed")
}
err := walkFn(resolvedPath, info, nil)
if err != nil {
if info.IsDir() && errors.Is(err, ErrWalkSkipDir) {
err = nil
}
return err
}
if resolvedPath != "" && info.Mode()&os.ModeSymlink == os.ModeSymlink {
// We only want to lstat on directories. If this entry is a symbolic link to a file, no need to recurse.
statInfo, err := os.Stat(resolvedPath)
if err != nil {
return err
}
if !statInfo.IsDir() {
return nil
}
path2, err := filepath.EvalSymlinks(resolvedPath)
if err != nil {
return err
}
// vout("SymLink Path: %v, links to: %v", resolvedPath, path2)
if symlinkPathsFollowed != nil {
if _, ok := symlinkPathsFollowed[path2]; ok {
errMsg := "potential symLink infinite loop, path: %v, link to: %v"
return fmt.Errorf(errMsg, resolvedPath, path2)
}
symlinkPathsFollowed[path2] = true
}
info2, err := os.Lstat(path2)
if err != nil {
return err
}
return w.walk(path, info2, path2, symlinkPathsFollowed, walkFn)
} else if info.IsDir() {
list, err := os.ReadDir(path)
if err != nil {
return walkFn(resolvedPath, info, err)
}
var subFiles = make([]subFile, 0)
for _, file := range list {
path2 := filepath.Join(path, file.Name())
var resolvedPath2 string
if resolvedPath != "" {
resolvedPath2 = filepath.Join(resolvedPath, file.Name())
}
fileInfo, err := file.Info()
if err != nil {
return fmt.Errorf("unable to read file info: %v, path: %v", file.Name(), path2)
}
subFiles = append(subFiles, subFile{path: path2, resolvedPath: resolvedPath2, fileInfo: fileInfo})
}
if w.containsDistFolder(subFiles) {
err := w.walk(
filepath.Join(path, "dist"),
info,
filepath.Join(resolvedPath, "dist"),
symlinkPathsFollowed,
walkFn)
if err != nil {
return err
}
} else {
for _, p := range subFiles {
err = w.walk(p.path, p.fileInfo, p.resolvedPath, symlinkPathsFollowed, walkFn)
if err != nil {
return err
}
}
}
return nil
}
return nil
}
// containsDistFolder returns true if the provided subFiles is a folder named "dist".
func (w *walker) containsDistFolder(subFiles []subFile) bool {
for _, p := range subFiles {
if p.fileInfo.IsDir() && p.fileInfo.Name() == "dist" {
return true
}
}
return false
}
type subFile struct {
path, resolvedPath string
fileInfo os.FileInfo
}
// CleanRelativePath returns the shortest path name equivalent to path
// by purely lexical processing. It makes sure the provided path is rooted
// and then uses filepath.Clean and filepath.Rel to make sure the path
// doesn't include any separators or elements that shouldn't be there
// like ., .., //.
func CleanRelativePath(path string) (string, error) {
cleanPath := filepath.Clean(filepath.Join("/", path))
rel, err := filepath.Rel("/", cleanPath)
if err != nil {
// slash is prepended above therefore this is not expected to fail
return "", err
}
return rel, nil
}
+39
View File
@@ -0,0 +1,39 @@
package plugins
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCleanRelativePath(t *testing.T) {
testcases := []struct {
input string
expectedPath string
}{
{
input: "",
expectedPath: ".",
},
{
input: filepath.Join(string(filepath.Separator), "test", "test.txt"),
expectedPath: filepath.Join("test", "test.txt"),
},
{
input: filepath.Join("..", "..", "test", "test.txt"),
expectedPath: filepath.Join("test", "test.txt"),
},
{
// since filepath.Join will remove the leading dot, we need to build the path manually
input: "." + string(filepath.Separator) + filepath.Join("..", "test", "test.txt"),
expectedPath: filepath.Join("test", "test.txt"),
},
}
for _, tt := range testcases {
path, err := CleanRelativePath(tt.input)
assert.NoError(t, err)
assert.Equal(t, tt.expectedPath, path)
}
}
+2 -4
View File
@@ -7,8 +7,6 @@ import (
"os"
"path/filepath"
"strings"
"github.com/grafana/grafana/pkg/util"
)
var (
@@ -110,7 +108,7 @@ func (f LocalFS) walkFunc(basePath string, acc map[string]struct{}) filepath.Wal
// If a nil error is returned, the caller should take care of calling Close() the returned fs.File.
// If the file does not exist, ErrFileNotExist is returned.
func (f LocalFS) Open(name string) (fs.File, error) {
cleanPath, err := util.CleanRelativePath(name)
cleanPath, err := CleanRelativePath(name)
if err != nil {
return nil, err
}
@@ -155,7 +153,7 @@ func (f LocalFS) Files() ([]string, error) {
if err != nil {
return nil, err
}
clenRelPath, err := util.CleanRelativePath(relPath)
clenRelPath, err := CleanRelativePath(relPath)
if err != nil {
continue
}
+5 -2
View File
@@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/util/proxyutil"
)
const (
@@ -124,7 +123,7 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq
res.Headers = map[string][]string{}
}
proxyutil.SetProxyResponseHeaders(res.Headers)
SetCSPHeader(res.Headers)
ensureContentTypeHeader(res)
}
@@ -281,6 +280,10 @@ func (s *Service) ValidateAdmission(ctx context.Context, req *backend.AdmissionR
return plugin.ValidateAdmission(ctx, req)
}
func SetCSPHeader(header http.Header) {
header.Set("Content-Security-Policy", "sandbox")
}
// plugin finds a plugin with `pluginID` from the registry that is not decommissioned
func (s *Service) plugin(ctx context.Context, pluginID, pluginVersion string) (*plugins.Plugin, bool) {
p, exists := s.pluginRegistry.Plugin(ctx, pluginID, pluginVersion)
+3 -5
View File
@@ -8,11 +8,11 @@ import (
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
"github.com/grafana/grafana/pkg/util/testutil"
"github.com/stretchr/testify/require"
)
func TestQueryData(t *testing.T) {
@@ -157,9 +157,7 @@ func TestCheckHealth(t *testing.T) {
})
}
func TestIntegrationCallResource(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
func TestCallResource(t *testing.T) {
registry := pluginfakes.NewFakePluginRegistry()
p := &plugins.Plugin{
JSONData: plugins.JSONData{
@@ -13,10 +13,9 @@ 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/util"
)
var walk = util.Walk
var walk = plugins.Walk
var (
ErrInvalidPluginJSONFilePath = errors.New("invalid plugin.json filepath was provided")
@@ -215,7 +214,7 @@ func (s *LocalSource) getAbsPluginJSONPaths(path string) ([]string, error) {
}
if fi.Name() == "node_modules" {
return util.ErrWalkSkipDir
return plugins.ErrWalkSkipDir
}
if fi.IsDir() {
-77
View File
@@ -1,77 +0,0 @@
package sources
import (
"context"
"path/filepath"
"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/setting"
)
type Service struct {
cfg *config.PluginManagementCfg
staticRootPath string
log log.Logger
}
func ProvideService(cfg *setting.Cfg, pCcfg *config.PluginManagementCfg) *Service {
return &Service{
cfg: pCcfg,
staticRootPath: cfg.StaticRootPath,
log: log.New("plugin.sources"),
}
}
func (s *Service) List(_ context.Context) []plugins.PluginSource {
r := []plugins.PluginSource{
NewLocalSource(
plugins.ClassCore,
s.corePluginPaths(),
),
}
r = append(r, s.externalPluginSources()...)
r = append(r, s.pluginSettingSources()...)
return r
}
func (s *Service) externalPluginSources() []plugins.PluginSource {
localSrcs, err := DirAsLocalSources(s.cfg, s.cfg.PluginsPath, plugins.ClassExternal)
if err != nil {
s.log.Error("Failed to load external plugins", "error", err)
return []plugins.PluginSource{}
}
srcs := make([]plugins.PluginSource, len(localSrcs))
for i, src := range localSrcs {
srcs[i] = src
}
return srcs
}
func (s *Service) pluginSettingSources() []plugins.PluginSource {
sources := make([]plugins.PluginSource, 0, len(s.cfg.PluginSettings))
for _, ps := range s.cfg.PluginSettings {
path, exists := ps["path"]
if !exists || path == "" {
continue
}
if s.cfg.DevMode {
sources = append(sources, NewUnsafeLocalSource(plugins.ClassExternal, []string{path}))
} else {
sources = append(sources, NewLocalSource(plugins.ClassExternal, []string{path}))
}
}
return sources
}
// corePluginPaths provides a list of the Core plugin file system paths
func (s *Service) corePluginPaths() []string {
datasourcePaths := filepath.Join(s.staticRootPath, "app", "plugins", "datasource")
panelsPath := filepath.Join(s.staticRootPath, "app", "plugins", "panel")
return []string{datasourcePaths, panelsPath}
}
-132
View File
@@ -1,132 +0,0 @@
package sources
import (
"context"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/setting"
)
func TestSources_List(t *testing.T) {
t.Run("Plugin sources are populated by default and listed in specific order", func(t *testing.T) {
testdata, err := filepath.Abs("../testdata")
require.NoError(t, err)
cfg := &setting.Cfg{
StaticRootPath: testdata,
}
pCfg := &config.PluginManagementCfg{
PluginsPath: filepath.Join(testdata, "pluginRootWithDist"),
PluginSettings: setting.PluginSettings{
"foo": map[string]string{
"path": filepath.Join(testdata, "test-app"),
},
"bar": map[string]string{
"url": "https://grafana.plugin",
},
},
}
s := ProvideService(cfg, pCfg)
srcs := s.List(context.Background())
ctx := context.Background()
require.Len(t, srcs, 5)
require.Equal(t, srcs[0].PluginClass(ctx), plugins.ClassCore)
if localSrc, ok := srcs[0].(*LocalSource); ok {
require.Equal(t, localSrc.Paths(), []string{
filepath.Join(testdata, "app", "plugins", "datasource"),
filepath.Join(testdata, "app", "plugins", "panel"),
})
} else {
t.Fatalf("Expected LocalSource, got %T", srcs[0])
}
sig, exists := srcs[0].DefaultSignature(ctx, "")
require.True(t, exists)
require.Equal(t, plugins.SignatureStatusInternal, sig.Status)
require.Equal(t, plugins.SignatureType(""), sig.Type)
require.Equal(t, "", sig.SigningOrg)
require.Equal(t, srcs[1].PluginClass(ctx), plugins.ClassExternal)
if localSrc, ok := srcs[1].(*LocalSource); ok {
require.Equal(t, localSrc.Paths(), []string{
filepath.Join(testdata, "pluginRootWithDist", "datasource"),
})
} else {
t.Fatalf("Expected LocalSource, got %T", srcs[1])
}
sig, exists = srcs[1].DefaultSignature(ctx, "")
require.False(t, exists)
require.Equal(t, plugins.Signature{}, sig)
require.Equal(t, srcs[2].PluginClass(ctx), plugins.ClassExternal)
if localSrc, ok := srcs[2].(*LocalSource); ok {
require.Equal(t, localSrc.Paths(), []string{
filepath.Join(testdata, "pluginRootWithDist", "dist"),
})
} else {
t.Fatalf("Expected LocalSource, got %T", srcs[2])
}
sig, exists = srcs[2].DefaultSignature(ctx, "")
require.False(t, exists)
require.Equal(t, plugins.Signature{}, sig)
require.Equal(t, srcs[3].PluginClass(ctx), plugins.ClassExternal)
if localSrc, ok := srcs[3].(*LocalSource); ok {
require.Equal(t, localSrc.Paths(), []string{
filepath.Join(testdata, "pluginRootWithDist", "panel"),
})
} else {
t.Fatalf("Expected LocalSource, got %T", srcs[3])
}
sig, exists = srcs[3].DefaultSignature(ctx, "")
require.False(t, exists)
require.Equal(t, plugins.Signature{}, sig)
})
t.Run("Plugin sources are populated with symbolic links", func(t *testing.T) {
testdata, err := filepath.Abs("../testdata")
require.NoError(t, err)
cfg := &setting.Cfg{
StaticRootPath: testdata,
}
pCfg := &config.PluginManagementCfg{
PluginsPath: filepath.Join(testdata, "symbolic-plugin-dirs"),
}
s := ProvideService(cfg, pCfg)
ctx := context.Background()
srcs := s.List(ctx)
uris := map[plugins.Class]map[string]struct{}{}
for _, src := range srcs {
class := src.PluginClass(ctx)
if _, exists := uris[class]; !exists {
uris[class] = map[string]struct{}{}
}
if localSrc, ok := src.(*LocalSource); ok {
for _, path := range localSrc.Paths() {
uris[class][path] = struct{}{}
}
}
}
require.Equal(t, uris[plugins.ClassCore], map[string]struct{}{
filepath.Join(testdata, "app", "plugins", "datasource"): {},
filepath.Join(testdata, "app", "plugins", "panel"): {},
}, "should include core plugins")
require.Equal(t, uris[plugins.ClassExternal], map[string]struct{}{
filepath.Join(testdata, "symbolic-plugin-dirs", "plugin"): {},
}, "should include external symlinked plugin")
})
}
+1 -2
View File
@@ -19,7 +19,6 @@ import (
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/util"
)
var (
@@ -414,7 +413,7 @@ func (p *Plugin) ConvertObjects(ctx context.Context, req *backend.ConversionRequ
}
func (p *Plugin) File(name string) (fs.File, error) {
cleanPath, err := util.CleanRelativePath(name)
cleanPath, err := CleanRelativePath(name)
if err != nil {
// CleanRelativePath should clean and make the path relative so this is not expected to fail
return nil, err