diff --git a/pkg/plugins/manager/loader/finder/finder.go b/pkg/plugins/manager/loader/finder/finder.go index 98ff7155f06..5893b8d20e2 100644 --- a/pkg/plugins/manager/loader/finder/finder.go +++ b/pkg/plugins/manager/loader/finder/finder.go @@ -19,20 +19,20 @@ func New() Finder { return Finder{log: log.New("plugin.finder")} } -func (f *Finder) Find(pluginDirs []string) ([]string, error) { +func (f *Finder) Find(pluginPaths []string) ([]string, error) { var pluginJSONPaths []string - for _, dir := range pluginDirs { - exists, err := fs.Exists(dir) + for _, path := range pluginPaths { + exists, err := fs.Exists(path) if err != nil { - f.log.Warn("Error occurred when checking if plugin directory exists", "dir", dir, "err", err) + f.log.Warn("Error occurred when checking if plugin directory exists", "path", path, "err", err) } if !exists { - f.log.Warn("Skipping finding plugins as directory does not exist", "dir", dir) + f.log.Warn("Skipping finding plugins as directory does not exist", "path", path) continue } - paths, err := f.getPluginJSONPaths(dir) + paths, err := f.getAbsPluginJSONPaths(path) if err != nil { return nil, err } @@ -42,16 +42,16 @@ func (f *Finder) Find(pluginDirs []string) ([]string, error) { return pluginJSONPaths, nil } -func (f *Finder) getPluginJSONPaths(dir string) ([]string, error) { +func (f *Finder) getAbsPluginJSONPaths(path string) ([]string, error) { var pluginJSONPaths []string var err error - dir, err = filepath.Abs(dir) + path, err = filepath.Abs(path) if err != nil { return []string{}, err } - if err := util.Walk(dir, true, true, + if err := util.Walk(path, true, true, func(currentPath string, fi os.FileInfo, err error) error { if err != nil { return fmt.Errorf("filepath.Walk reported an error for %q: %w", currentPath, err) @@ -73,11 +73,11 @@ func (f *Finder) getPluginJSONPaths(dir string) ([]string, error) { return nil }); err != nil { if errors.Is(err, os.ErrNotExist) { - f.log.Debug("Couldn't scan directory since it doesn't exist", "pluginDir", dir, "err", err) + f.log.Debug("Couldn't scan directory since it doesn't exist", "pluginDir", path, "err", err) return []string{}, nil } if errors.Is(err, os.ErrPermission) { - f.log.Debug("Couldn't scan directory due to lack of permissions", "pluginDir", dir, "err", err) + f.log.Debug("Couldn't scan directory due to lack of permissions", "pluginDir", path, "err", err) return []string{}, nil } diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 57aa2cd9f4d..41f470fa758 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -97,12 +97,7 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSO // calculate initial signature state loadedPlugins := make(map[string]*plugins.Plugin) for pluginDir, pluginJSON := range foundPlugins { - plugin := &plugins.Plugin{ - JSONData: pluginJSON, - PluginDir: pluginDir, - Class: class, - } - plugin.SetLogger(l.log.New("pluginID", plugin.ID)) + plugin := createPluginBase(pluginJSON, class, pluginDir, l.log) sig, err := signature.Calculate(l.log, plugin) if err != nil { @@ -165,15 +160,23 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSO } } + if plugin.IsApp() { + setDefaultNavURL(plugin, l.cfg.AppSubURL) + } + + if plugin.Parent != nil && plugin.Parent.IsApp() { + configureAppChildOPlugin(plugin.Parent, plugin) + } + verifiedPlugins = append(verifiedPlugins, plugin) } for _, p := range verifiedPlugins { - l.setDefaults(p) err := l.pluginInitializer.Initialize(ctx, p) if err != nil { return nil, err } + metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature)) } return verifiedPlugins, nil @@ -228,56 +231,49 @@ func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) return plugin, nil } -func (l *Loader) setDefaults(p *plugins.Plugin) { - setModule(p) +func createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string, logger log.Logger) *plugins.Plugin { + plugin := &plugins.Plugin{ + JSONData: pluginJSON, + PluginDir: pluginDir, + BaseURL: baseURL(pluginJSON, class, pluginDir), + Module: module(pluginJSON, class, pluginDir), + Class: class, + } + plugin.SetLogger(logger.New("pluginID", plugin.ID)) + setImages(plugin) + + return plugin +} + +func setImages(p *plugins.Plugin) { p.Info.Logos.Small = pluginLogoURL(p.Type, p.Info.Logos.Small, p.BaseURL) p.Info.Logos.Large = pluginLogoURL(p.Type, p.Info.Logos.Large, p.BaseURL) for i := 0; i < len(p.Info.Screenshots); i++ { p.Info.Screenshots[i].Path = evalRelativePluginURLPath(p.Info.Screenshots[i].Path, p.BaseURL, p.Type) } +} - if p.IsApp() { - for _, child := range p.Children { - setPathsBasedOnApp(p, child) +func setDefaultNavURL(p *plugins.Plugin, appSubURL string) { + // slugify pages + for _, include := range p.Includes { + if include.Slug == "" { + include.Slug = slug.Make(include.Name) } - - // slugify pages - for _, include := range p.Includes { - if include.Slug == "" { - include.Slug = slug.Make(include.Name) - } - if include.Type == "page" && include.DefaultNav { - p.DefaultNavURL = l.cfg.AppSubURL + "/plugins/" + p.ID + "/page/" + include.Slug - } - if include.Type == "dashboard" && include.DefaultNav { - p.DefaultNavURL = l.cfg.AppSubURL + "/dashboard/db/" + include.Slug - } + if include.Type == "page" && include.DefaultNav { + p.DefaultNavURL = appSubURL + "/plugins/" + p.ID + "/page/" + include.Slug + } + if include.Type == "dashboard" && include.DefaultNav { + p.DefaultNavURL = appSubURL + "/dashboard/db/" + include.Slug } } } -func setModule(p *plugins.Plugin) { - if p.IsCorePlugin() { - // Previously there was an assumption that the Core plugins directory - // should be public/app/plugins// - // However this can be an issue if the Core plugins directory is renamed - baseDir := filepath.Base(p.PluginDir) - - // use path package for the following statements because these are not file paths - p.Module = path.Join("app/plugins", string(p.Type), baseDir, "module") - p.BaseURL = path.Join("public/app/plugins", string(p.Type), baseDir) +func configureAppChildOPlugin(parent *plugins.Plugin, child *plugins.Plugin) { + if !parent.IsApp() { return } - - metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature)) - - p.Module = path.Join("plugins", p.ID, "module") - p.BaseURL = path.Join("public/plugins", p.ID) -} - -func setPathsBasedOnApp(parent *plugins.Plugin, child *plugins.Plugin) { appSubPath := strings.ReplaceAll(strings.Replace(child.PluginDir, parent.PluginDir, "", 1), "\\", "/") child.IncludedInAppID = parent.ID child.BaseURL = parent.BaseURL @@ -331,6 +327,22 @@ func (l *Loader) PluginErrors() []*plugins.Error { return errs } +func baseURL(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) string { + if class == plugins.Core { + return path.Join("public/app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir)) + } + + return path.Join("public/plugins", pluginJSON.ID) +} + +func module(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) string { + if class == plugins.Core { + return path.Join("app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir), "module") + } + + return path.Join("plugins", pluginJSON.ID, "module") +} + func validatePluginJSON(data plugins.JSONData) error { if data.ID == "" || !data.Type.IsValid() { return ErrInvalidPluginJSON diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 6ae860f48aa..e178cd820d9 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -638,8 +638,7 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { } func TestLoader_loadNestedPlugins(t *testing.T) { - t.Skip() - parentDir, err := filepath.Abs("../") + rootDir, err := filepath.Abs("../") if err != nil { t.Errorf("could not construct absolute path of root dir") return @@ -670,7 +669,7 @@ func TestLoader_loadNestedPlugins(t *testing.T) { }, Module: "plugins/test-ds/module", BaseURL: "public/plugins/test-ds", - PluginDir: filepath.Join(parentDir, "testdata/nested-plugins/parent"), + PluginDir: filepath.Join(rootDir, "testdata/nested-plugins/parent"), Signature: plugins.SignatureValid, SignatureType: plugins.GrafanaSignature, SignatureOrg: "Grafana Labs", @@ -702,7 +701,7 @@ func TestLoader_loadNestedPlugins(t *testing.T) { }, Module: "plugins/test-panel/module", BaseURL: "public/plugins/test-panel", - PluginDir: filepath.Join(parentDir, "testdata/nested-plugins/parent/nested"), + PluginDir: filepath.Join(rootDir, "testdata/nested-plugins/parent/nested"), Signature: plugins.SignatureValid, SignatureType: plugins.GrafanaSignature, SignatureOrg: "Grafana Labs", @@ -715,7 +714,7 @@ func TestLoader_loadNestedPlugins(t *testing.T) { t.Run("Load nested External plugins", func(t *testing.T) { expected := []*plugins.Plugin{parent, child} l := newLoader(&plugins.Cfg{ - PluginsPath: parentDir, + PluginsPath: rootDir, }) got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}, map[string]struct{}{}) @@ -737,7 +736,7 @@ func TestLoader_loadNestedPlugins(t *testing.T) { expected := []*plugins.Plugin{parent} l := newLoader(&plugins.Cfg{ - PluginsPath: parentDir, + PluginsPath: rootDir, }) got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}, map[string]struct{}{ @@ -828,7 +827,7 @@ func TestLoader_loadNestedPlugins(t *testing.T) { }, Module: "plugins/myorgid-simple-app/module", BaseURL: "public/plugins/myorgid-simple-app", - PluginDir: filepath.Join(parentDir, "testdata/app-with-child/dist"), + PluginDir: filepath.Join(rootDir, "testdata/app-with-child/dist"), DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react", Signature: plugins.SignatureValid, SignatureType: plugins.GrafanaSignature, @@ -866,7 +865,7 @@ func TestLoader_loadNestedPlugins(t *testing.T) { }, Module: "plugins/myorgid-simple-app/child/module", BaseURL: "public/plugins/myorgid-simple-app", - PluginDir: filepath.Join(parentDir, "testdata/app-with-child/dist/child"), + PluginDir: filepath.Join(rootDir, "testdata/app-with-child/dist/child"), IncludedInAppID: parent.ID, Signature: plugins.SignatureValid, SignatureType: plugins.GrafanaSignature, @@ -879,7 +878,7 @@ func TestLoader_loadNestedPlugins(t *testing.T) { expected := []*plugins.Plugin{parent, child} l := newLoader(&plugins.Cfg{ - PluginsPath: parentDir, + PluginsPath: rootDir, }) got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/app-with-child"}, map[string]struct{}{}) @@ -893,6 +892,39 @@ func TestLoader_loadNestedPlugins(t *testing.T) { if !cmp.Equal(got, expected, compareOpts) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) } + + t.Run("order of loaded parent and child plugins gives same output", func(t *testing.T) { + parentPluginJSON := filepath.Join(rootDir, "testdata/app-with-child/dist/plugin.json") + childPluginJSON := filepath.Join(rootDir, "testdata/app-with-child/dist/child/plugin.json") + + got, err := l.loadPlugins(context.Background(), plugins.External, []string{ + parentPluginJSON, childPluginJSON}, + map[string]struct{}{}) + assert.NoError(t, err) + + // to ensure we can compare with expected + sort.SliceStable(got, func(i, j int) bool { + return got[i].ID < got[j].ID + }) + + if !cmp.Equal(got, expected, compareOpts) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) + } + + got, err = l.loadPlugins(context.Background(), plugins.External, []string{ + childPluginJSON, parentPluginJSON}, + map[string]struct{}{}) + assert.NoError(t, err) + + // to ensure we can compare with expected + sort.SliceStable(got, func(i, j int) bool { + return got[i].ID < got[j].ID + }) + + if !cmp.Equal(got, expected, compareOpts) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) + } + }) }) } @@ -1028,14 +1060,15 @@ func Test_setPathsBasedOnApp(t *testing.T) { } parent := &plugins.Plugin{ JSONData: plugins.JSONData{ - ID: "testdata", + Type: plugins.App, + ID: "testdata", }, Class: plugins.Core, PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata", BaseURL: "public/app/plugins/app/testdata", } - setPathsBasedOnApp(parent, child) + configureAppChildOPlugin(parent, child) assert.Equal(t, "app/plugins/app/testdata/datasources/datasource/module", child.Module) assert.Equal(t, "testdata", child.IncludedInAppID)