diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 994da69c0af..eb15d1a5f84 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -127,7 +127,7 @@ func osAndArchString() string { return osString + "-" + arch } -func supportsCurrentArch(version *models.Version) bool { +func supportsCurrentArch(version models.Version) bool { if version.Arch == nil { return true } @@ -139,10 +139,10 @@ func supportsCurrentArch(version *models.Version) bool { return false } -func latestSupportedVersion(plugin *models.Plugin) *models.Version { +func latestSupportedVersion(plugin models.Plugin) *models.Version { for _, v := range plugin.Versions { ver := v - if supportsCurrentArch(&ver) { + if supportsCurrentArch(ver) { return &ver } } diff --git a/pkg/cmd/grafana-cli/commands/listremote_command.go b/pkg/cmd/grafana-cli/commands/listremote_command.go index 79b11c939b1..ba110b00026 100644 --- a/pkg/cmd/grafana-cli/commands/listremote_command.go +++ b/pkg/cmd/grafana-cli/commands/listremote_command.go @@ -15,11 +15,10 @@ func listRemoteCommand(c utils.CommandLine) error { } for _, p := range plugin.Plugins { - plugin := p - if len(plugin.Versions) > 0 { - ver := latestSupportedVersion(&plugin) + if len(p.Versions) > 0 { + ver := latestSupportedVersion(p) if ver != nil { - logger.Infof("id: %v version: %s\n", plugin.ID, ver.Version) + logger.Infof("id: %v version: %s\n", p.ID, ver.Version) } } } diff --git a/pkg/cmd/grafana-cli/commands/listversions_command.go b/pkg/cmd/grafana-cli/commands/listversions_command.go index 89ca4ffaa6f..ca6cd4edc5d 100644 --- a/pkg/cmd/grafana-cli/commands/listversions_command.go +++ b/pkg/cmd/grafana-cli/commands/listversions_command.go @@ -24,7 +24,7 @@ func listVersionsCommand(c utils.CommandLine) error { pluginToList := c.Args().First() - plugin, err := services.GetPlugin(pluginToList, c.String("repo")) + plugin, err := services.GetPluginInfoFromRepo(pluginToList, c.String("repo")) if err != nil { return err } diff --git a/pkg/cmd/grafana-cli/commands/ls_command.go b/pkg/cmd/grafana-cli/commands/ls_command.go index 8eb06bb2925..76132fdb327 100644 --- a/pkg/cmd/grafana-cli/commands/ls_command.go +++ b/pkg/cmd/grafana-cli/commands/ls_command.go @@ -6,13 +6,10 @@ import ( "github.com/fatih/color" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" ) -var ls_getPlugins func(path string) []models.InstalledPlugin = services.GetLocalPlugins - var ( errMissingPathFlag = errors.New("missing path flag") errNotDirectory = errors.New("plugin path is not a directory") @@ -41,7 +38,7 @@ func lsCommand(c utils.CommandLine) error { return err } - plugins := ls_getPlugins(pluginDir) + plugins := services.GetLocalPlugins(pluginDir) if len(plugins) > 0 { logger.Info("installed plugins:\n") @@ -50,7 +47,8 @@ func lsCommand(c utils.CommandLine) error { } for _, plugin := range plugins { - logger.Infof("%s %s %s\n", plugin.ID, color.YellowString("@"), plugin.Info.Version) + logger.Infof("%s %s %s\n", plugin.Primary.JSONData.ID, + color.YellowString("@"), plugin.Primary.JSONData.Info.Version) } return nil diff --git a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go index b13ddea45f7..74b3e65f038 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go @@ -9,10 +9,15 @@ import ( "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" + "github.com/grafana/grafana/pkg/plugins" ) -func shouldUpgrade(installed string, remote *models.Plugin) bool { - installedVersion, err := version.NewVersion(installed) +func shouldUpgrade(installed plugins.FoundPlugin, remote models.Plugin) bool { + installedVer := installed.JSONData.Info.Version + if installedVer == "" { + installedVer = "0.0.0" + } + installedVersion, err := version.NewVersion(installedVer) if err != nil { return false } @@ -35,30 +40,30 @@ func upgradeAllCommand(c utils.CommandLine) error { return err } - pluginsToUpgrade := make([]models.InstalledPlugin, 0) + pluginsToUpgrade := make([]plugins.FoundPlugin, 0) for _, localPlugin := range localPlugins { for _, p := range remotePlugins.Plugins { remotePlugin := p - if localPlugin.ID != remotePlugin.ID { + if localPlugin.Primary.JSONData.ID != remotePlugin.ID { continue } - if shouldUpgrade(localPlugin.Info.Version, &remotePlugin) { - pluginsToUpgrade = append(pluginsToUpgrade, localPlugin) + if shouldUpgrade(localPlugin.Primary, remotePlugin) { + pluginsToUpgrade = append(pluginsToUpgrade, localPlugin.Primary) } } } ctx := context.Background() for _, p := range pluginsToUpgrade { - logger.Infof("Updating %v \n", p.ID) + logger.Infof("Updating %v \n", p.JSONData.ID) - err = uninstallPlugin(ctx, p.ID, c) + err = uninstallPlugin(ctx, p.JSONData.ID, c) if err != nil { return err } - err = installPlugin(ctx, p.ID, "", c) + err = installPlugin(ctx, p.JSONData.ID, "", c) if err != nil { return err } diff --git a/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go b/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go index c69f0a18a08..6749972f950 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go @@ -4,9 +4,10 @@ import ( "fmt" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" + "github.com/grafana/grafana/pkg/plugins" ) func TestVersionComparison(t *testing.T) { @@ -16,15 +17,23 @@ func TestVersionComparison(t *testing.T) { {Version: "2.0.0"}, } - upgradeablePlugins := map[string]models.Plugin{ - "0.0.0": {Versions: versions}, - "1.0.0": {Versions: versions}, + upgradeablePlugins := []struct { + have plugins.FoundPlugin + requested models.Plugin + }{ + { + have: plugins.FoundPlugin{JSONData: plugins.JSONData{Info: plugins.Info{Version: "0.0.0"}}}, + requested: models.Plugin{Versions: versions}, + }, + { + have: plugins.FoundPlugin{JSONData: plugins.JSONData{Info: plugins.Info{Version: "1.0.0"}}}, + requested: models.Plugin{Versions: versions}, + }, } - for k, v := range upgradeablePlugins { - val := v - t.Run(fmt.Sprintf("for %s should be true", k), func(t *testing.T) { - assert.True(t, shouldUpgrade(k, &val)) + for _, v := range upgradeablePlugins { + t.Run(fmt.Sprintf("for %s should be true", v.have.JSONData.Info.Version), func(t *testing.T) { + require.True(t, shouldUpgrade(v.have, v.requested)) }) } }) @@ -35,15 +44,23 @@ func TestVersionComparison(t *testing.T) { {Version: "2.0.0"}, } - shouldNotUpgrade := map[string]models.Plugin{ - "2.0.0": {Versions: versions}, - "6.0.0": {Versions: versions}, + shouldNotUpgrade := []struct { + have plugins.FoundPlugin + requested models.Plugin + }{ + { + have: plugins.FoundPlugin{JSONData: plugins.JSONData{Info: plugins.Info{Version: "2.0.0"}}}, + requested: models.Plugin{Versions: versions}, + }, + { + have: plugins.FoundPlugin{JSONData: plugins.JSONData{Info: plugins.Info{Version: "6.0.0"}}}, + requested: models.Plugin{Versions: versions}, + }, } - for k, v := range shouldNotUpgrade { - val := v - t.Run(fmt.Sprintf("for %s should be false", k), func(t *testing.T) { - assert.False(t, shouldUpgrade(k, &val)) + for _, v := range shouldNotUpgrade { + t.Run(fmt.Sprintf("for %s should be false", v.have.JSONData.Info.Version), func(t *testing.T) { + require.False(t, shouldUpgrade(v.have, v.requested)) }) } }) diff --git a/pkg/cmd/grafana-cli/commands/upgrade_command.go b/pkg/cmd/grafana-cli/commands/upgrade_command.go index cf7d47ec9d2..a122f55b2fc 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_command.go @@ -16,17 +16,17 @@ func upgradeCommand(c utils.CommandLine) error { pluginsDir := c.PluginDirectory() pluginID := c.Args().First() - localPlugin, err := services.ReadPlugin(pluginsDir, pluginID) + localPlugin, err := services.GetLocalPlugin(pluginsDir, pluginID) if err != nil { return err } - plugin, err := services.GetPlugin(pluginID, c.PluginRepoURL()) + plugin, err := services.GetPluginInfoFromRepo(pluginID, c.PluginRepoURL()) if err != nil { return err } - if shouldUpgrade(localPlugin.Info.Version, &plugin) { + if shouldUpgrade(localPlugin, plugin) { if err = uninstallPlugin(ctx, pluginID, c); err != nil { return fmt.Errorf("failed to remove plugin '%s': %w", pluginID, err) } diff --git a/pkg/cmd/grafana-cli/services/api_client.go b/pkg/cmd/grafana-cli/services/api_client.go index 99d3c9b7f93..70746718d70 100644 --- a/pkg/cmd/grafana-cli/services/api_client.go +++ b/pkg/cmd/grafana-cli/services/api_client.go @@ -14,7 +14,7 @@ import ( "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" ) -func GetPlugin(pluginId, repoUrl string) (models.Plugin, error) { +func GetPluginInfoFromRepo(pluginId, repoUrl string) (models.Plugin, error) { logger.Debugf("getting plugin metadata from: %v pluginId: %v \n", repoUrl, pluginId) body, err := sendRequestGetBytes(HttpClient, repoUrl, "repo", pluginId) if err != nil { diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index 119e9f91199..6d042348d42 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -1,8 +1,8 @@ package services import ( + "context" "crypto/tls" - "encoding/json" "errors" "fmt" "net" @@ -12,6 +12,10 @@ import ( "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/sources" ) var ( @@ -62,43 +66,25 @@ func makeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client { } } -func ReadPlugin(pluginDir, pluginName string) (models.InstalledPlugin, error) { - distPluginDataPath := filepath.Join(pluginDir, pluginName, "dist", "plugin.json") +func GetLocalPlugin(pluginDir, pluginID string) (plugins.FoundPlugin, error) { + pluginPath := filepath.Join(pluginDir, pluginID) - data, err := IoHelper.ReadFile(distPluginDataPath) + ps := GetLocalPlugins(pluginPath) + if len(ps) == 0 { + return plugins.FoundPlugin{}, errors.New("could not find plugin " + pluginID + " in " + pluginDir) + } + + return ps[0].Primary, nil +} + +func GetLocalPlugins(pluginDir string) []*plugins.FoundBundle { + f := finder.NewLocalFinder(&config.Cfg{}) + + res, err := f.Find(context.Background(), sources.NewLocalSource(plugins.External, []string{pluginDir})) if err != nil { - pluginDataPath := filepath.Join(pluginDir, pluginName, "plugin.json") - data, err = IoHelper.ReadFile(pluginDataPath) - if err != nil { - return models.InstalledPlugin{}, errors.New("Could not find dist/plugin.json or plugin.json for " + pluginName + " in " + pluginDir) - } + logger.Error("Could not get local plugins", err) + return make([]*plugins.FoundBundle, 0) } - res := models.InstalledPlugin{} - if err := json.Unmarshal(data, &res); err != nil { - return res, err - } - - if res.Info.Version == "" { - res.Info.Version = "0.0.0" - } - - if res.ID == "" { - return models.InstalledPlugin{}, errors.New("could not find plugin " + pluginName + " in " + pluginDir) - } - - return res, nil -} - -func GetLocalPlugins(pluginDir string) []models.InstalledPlugin { - result := make([]models.InstalledPlugin, 0) - files, _ := IoHelper.ReadDir(pluginDir) - for _, f := range files { - res, err := ReadPlugin(pluginDir, f.Name()) - if err == nil { - result = append(result, res) - } - } - - return result + return res } diff --git a/pkg/plugins/manager/loader/finder/local.go b/pkg/plugins/manager/loader/finder/local.go index a3217697a60..a192cf916cb 100644 --- a/pkg/plugins/manager/loader/finder/local.go +++ b/pkg/plugins/manager/loader/finder/local.go @@ -20,7 +20,6 @@ import ( var walk = util.Walk var ( - ErrInvalidPluginJSON = errors.New("did not find valid type or id properties in plugin.json") ErrInvalidPluginJSONFilePath = errors.New("invalid plugin.json filepath was provided") ) @@ -149,7 +148,7 @@ func (l *Local) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) l.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "err", err) return plugins.JSONData{}, err } - plugin, err := ReadPluginJSON(reader) + plugin, err := plugins.ReadPluginJSON(reader) if err != nil { l.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "err", err) return plugins.JSONData{}, err diff --git a/pkg/plugins/manager/loader/finder/local_test.go b/pkg/plugins/manager/loader/finder/local_test.go index e6b4f031e46..1a37aa977b4 100644 --- a/pkg/plugins/manager/loader/finder/local_test.go +++ b/pkg/plugins/manager/loader/finder/local_test.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsintegration/config" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -331,127 +330,6 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) { }) } -func TestFinder_validatePluginJSON(t *testing.T) { - type args struct { - data plugins.JSONData - } - tests := []struct { - name string - args args - err error - }{ - { - name: "Valid case", - args: args{ - data: plugins.JSONData{ - ID: "grafana-plugin-id", - Type: plugins.DataSource, - }, - }, - }, - { - name: "Invalid plugin ID", - args: args{ - data: plugins.JSONData{ - Type: plugins.Panel, - }, - }, - err: ErrInvalidPluginJSON, - }, - { - name: "Invalid plugin type", - args: args{ - data: plugins.JSONData{ - ID: "grafana-plugin-id", - Type: "test", - }, - }, - err: ErrInvalidPluginJSON, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := validatePluginJSON(tt.args.data); !errors.Is(err, tt.err) { - t.Errorf("validatePluginJSON() = %v, want %v", err, tt.err) - } - }) - } -} - -func TestFinder_readPluginJSON(t *testing.T) { - tests := []struct { - name string - pluginPath string - expected plugins.JSONData - err error - }{ - { - name: "Valid plugin", - pluginPath: "../../testdata/test-app/plugin.json", - expected: plugins.JSONData{ - ID: "test-app", - Type: "app", - Name: "Test App", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Test Inc.", - URL: "http://test.com", - }, - Description: "Official Grafana Test App & Dashboard bundle", - Version: "1.0.0", - Links: []plugins.InfoLink{ - {Name: "Project site", URL: "http://project.com"}, - {Name: "License & Terms", URL: "http://license.com"}, - }, - Logos: plugins.Logos{ - Small: "img/logo_small.png", - Large: "img/logo_large.png", - }, - Screenshots: []plugins.Screenshots{ - {Path: "img/screenshot1.png", Name: "img1"}, - {Path: "img/screenshot2.png", Name: "img2"}, - }, - Updated: "2015-02-10", - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "3.x.x", - Plugins: []plugins.Dependency{ - {Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"}, - {Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"}, - }, - }, - Includes: []*plugins.Includes{ - {Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer}, - {Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer}, - {Name: "Nginx Panel", Type: "panel", Role: org.RoleViewer}, - {Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer}, - }, - Backend: false, - }, - }, - { - name: "Invalid plugin JSON", - pluginPath: "../../testdata/invalid-plugin-json/plugin.json", - err: ErrInvalidPluginJSON, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - reader, err := os.Open(tt.pluginPath) - require.NoError(t, err) - got, err := ReadPluginJSON(reader) - if tt.err != nil { - require.ErrorIs(t, err, tt.err) - } - if !cmp.Equal(got, tt.expected) { - t.Errorf("Unexpected pluginJSONData: %v", cmp.Diff(got, tt.expected)) - } - require.NoError(t, reader.Close()) - }) - } -} - var fsComparer = cmp.Comparer(func(fs1 plugins.FS, fs2 plugins.FS) bool { fs1Files, err := fs1.Files() if err != nil { diff --git a/pkg/plugins/manager/loader/finder/util.go b/pkg/plugins/manager/loader/finder/util.go deleted file mode 100644 index 9502a408be2..00000000000 --- a/pkg/plugins/manager/loader/finder/util.go +++ /dev/null @@ -1,47 +0,0 @@ -package finder - -import ( - "encoding/json" - "io" - - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/org" -) - -func ReadPluginJSON(reader io.Reader) (plugins.JSONData, error) { - plugin := plugins.JSONData{} - if err := json.NewDecoder(reader).Decode(&plugin); err != nil { - return plugins.JSONData{}, err - } - - if err := validatePluginJSON(plugin); err != nil { - return plugins.JSONData{}, err - } - - if plugin.ID == "grafana-piechart-panel" { - plugin.Name = "Pie Chart (old)" - } - - if len(plugin.Dependencies.Plugins) == 0 { - plugin.Dependencies.Plugins = []plugins.Dependency{} - } - - if plugin.Dependencies.GrafanaVersion == "" { - plugin.Dependencies.GrafanaVersion = "*" - } - - for _, include := range plugin.Includes { - if include.Role == "" { - include.Role = org.RoleViewer - } - } - - return plugin, nil -} - -func validatePluginJSON(data plugins.JSONData) error { - if data.ID == "" || !data.Type.IsValid() { - return ErrInvalidPluginJSON - } - return nil -} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index fd27a43c15c..a102ff7e71a 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/fs" "path" "runtime" @@ -24,6 +25,7 @@ var ( ErrFileNotExist = errors.New("file does not exist") ErrPluginFileRead = errors.New("file could not be read") ErrUninstallInvalidPluginDir = errors.New("cannot recognize as plugin folder") + ErrInvalidPluginJSON = errors.New("did not find valid type or id properties in plugin.json") ) type Plugin struct { @@ -139,6 +141,44 @@ type JSONData struct { Executable string `json:"executable,omitempty"` } +func ReadPluginJSON(reader io.Reader) (JSONData, error) { + plugin := JSONData{} + if err := json.NewDecoder(reader).Decode(&plugin); err != nil { + return JSONData{}, err + } + + if err := validatePluginJSON(plugin); err != nil { + return JSONData{}, err + } + + if plugin.ID == "grafana-piechart-panel" { + plugin.Name = "Pie Chart (old)" + } + + if len(plugin.Dependencies.Plugins) == 0 { + plugin.Dependencies.Plugins = []Dependency{} + } + + if plugin.Dependencies.GrafanaVersion == "" { + plugin.Dependencies.GrafanaVersion = "*" + } + + for _, include := range plugin.Includes { + if include.Role == "" { + include.Role = org.RoleViewer + } + } + + return plugin, nil +} + +func validatePluginJSON(data JSONData) error { + if data.ID == "" || !data.Type.IsValid() { + return ErrInvalidPluginJSON + } + return nil +} + func (d JSONData) DashboardIncludes() []*Includes { result := []*Includes{} for _, include := range d.Includes { diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go new file mode 100644 index 00000000000..e19e366f457 --- /dev/null +++ b/pkg/plugins/plugins_test.go @@ -0,0 +1,167 @@ +package plugins + +import ( + "errors" + "io" + "os" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/grafana/grafana/pkg/services/org" + "github.com/stretchr/testify/require" +) + +func Test_ReadPluginJSON(t *testing.T) { + tests := []struct { + name string + pluginJSON func(t *testing.T) io.ReadCloser + expected JSONData + err error + }{ + { + name: "Valid plugin", + pluginJSON: func(t *testing.T) io.ReadCloser { + reader, err := os.Open("manager/testdata/test-app/plugin.json") + require.NoError(t, err) + return reader + }, + expected: JSONData{ + ID: "test-app", + Type: "app", + Name: "Test App", + Info: Info{ + Author: InfoLink{ + Name: "Test Inc.", + URL: "http://test.com", + }, + Description: "Official Grafana Test App & Dashboard bundle", + Version: "1.0.0", + Links: []InfoLink{ + {Name: "Project site", URL: "http://project.com"}, + {Name: "License & Terms", URL: "http://license.com"}, + }, + Logos: Logos{ + Small: "img/logo_small.png", + Large: "img/logo_large.png", + }, + Screenshots: []Screenshots{ + {Path: "img/screenshot1.png", Name: "img1"}, + {Path: "img/screenshot2.png", Name: "img2"}, + }, + Updated: "2015-02-10", + }, + Dependencies: Dependencies{ + GrafanaVersion: "3.x.x", + Plugins: []Dependency{ + {Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"}, + {Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"}, + }, + }, + Includes: []*Includes{ + {Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer}, + {Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer}, + {Name: "Nginx Panel", Type: "panel", Role: org.RoleViewer}, + {Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer}, + }, + Backend: false, + }, + }, + { + name: "Invalid plugin JSON", + pluginJSON: func(t *testing.T) io.ReadCloser { + reader, err := os.Open("manager/testdata/invalid-plugin-json/plugin.json") + require.NoError(t, err) + return reader + }, + err: ErrInvalidPluginJSON, + }, + { + name: "Default value overrides", + pluginJSON: func(t *testing.T) io.ReadCloser { + pJSON := `{ + "id": "grafana-piechart-panel", + "name": "This will be overwritten", + "type": "panel", + "includes": [ + {"type": "dashboard", "name": "Pie Charts", "path": "dashboards/demo.json"} + ] + }` + return io.NopCloser(strings.NewReader(pJSON)) + }, + expected: JSONData{ + ID: "grafana-piechart-panel", + Type: "panel", + Name: "Pie Chart (old)", + Dependencies: Dependencies{ + GrafanaVersion: "*", + Plugins: []Dependency{}, + }, + Includes: []*Includes{ + {Name: "Pie Charts", Path: "dashboards/demo.json", Type: "dashboard", Role: org.RoleViewer}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := tt.pluginJSON(t) + got, err := ReadPluginJSON(p) + if tt.err != nil { + require.ErrorIs(t, err, tt.err) + } + if !cmp.Equal(got, tt.expected) { + t.Errorf("Unexpected pluginJSONData: %v", cmp.Diff(got, tt.expected)) + } + require.NoError(t, p.Close()) + }) + } +} + +func Test_validatePluginJSON(t *testing.T) { + type args struct { + data JSONData + } + tests := []struct { + name string + args args + err error + }{ + { + name: "Valid case", + args: args{ + data: JSONData{ + ID: "grafana-plugin-id", + Type: DataSource, + }, + }, + }, + { + name: "Invalid plugin ID", + args: args{ + data: JSONData{ + Type: Panel, + }, + }, + err: ErrInvalidPluginJSON, + }, + { + name: "Invalid plugin type", + args: args{ + data: JSONData{ + ID: "grafana-plugin-id", + Type: "test", + }, + }, + err: ErrInvalidPluginJSON, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validatePluginJSON(tt.args.data); !errors.Is(err, tt.err) { + t.Errorf("validatePluginJSON() = %v, want %v", err, tt.err) + } + }) + } +} diff --git a/pkg/plugins/storage/fs.go b/pkg/plugins/storage/fs.go index f3468ab3f23..445ce07c3f9 100644 --- a/pkg/plugins/storage/fs.go +++ b/pkg/plugins/storage/fs.go @@ -4,7 +4,6 @@ import ( "archive/zip" "bytes" "context" - "encoding/json" "errors" "fmt" "io" @@ -13,6 +12,7 @@ import ( "regexp" "strings" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/log" ) @@ -39,15 +39,15 @@ func (fs *FS) Extract(ctx context.Context, pluginID string, pluginArchive *zip.R return nil, fmt.Errorf("%v: %w", "failed to extract plugin archive", err) } - res, err := toPluginDTO(pluginID, pluginDir) + pluginJSON, err := readPluginJSON(pluginID, pluginDir) if err != nil { return nil, fmt.Errorf("%v: %w", "failed to convert to plugin DTO", err) } - fs.log.Successf("Downloaded and extracted %s v%s zip successfully to %s", res.ID, res.Info.Version, pluginDir) + fs.log.Successf("Downloaded and extracted %s v%s zip successfully to %s", pluginJSON.ID, pluginJSON.Info.Version, pluginDir) - deps := make([]*Dependency, 0, len(res.Dependencies.Plugins)) - for _, plugin := range res.Dependencies.Plugins { + deps := make([]*Dependency, 0, len(pluginJSON.Dependencies.Plugins)) + for _, plugin := range pluginJSON.Dependencies.Plugins { deps = append(deps, &Dependency{ ID: plugin.ID, Version: plugin.Version, @@ -55,8 +55,8 @@ func (fs *FS) Extract(ctx context.Context, pluginID string, pluginArchive *zip.R } return &ExtractedPluginArchive{ - ID: res.ID, - Version: res.Info.Version, + ID: pluginJSON.ID, + Version: pluginJSON.Info.Version, Dependencies: deps, Path: pluginDir, }, nil @@ -220,34 +220,26 @@ func removeGitBuildFromName(filename, pluginID string) string { return reGitBuild.ReplaceAllString(filename, pluginID+"/") } -func toPluginDTO(pluginID, pluginDir string) (installedPlugin, error) { - distPluginDataPath := filepath.Join(pluginDir, "dist", "plugin.json") +func readPluginJSON(pluginID, pluginDir string) (plugins.JSONData, error) { + pluginPath := filepath.Join(pluginDir, "plugin.json") // It's safe to ignore gosec warning G304 since the file path suffix is hardcoded // nolint:gosec - data, err := os.ReadFile(distPluginDataPath) + data, err := os.ReadFile(pluginPath) if err != nil { - pluginDataPath := filepath.Join(pluginDir, "plugin.json") + pluginPath = filepath.Join(pluginDir, "dist", "plugin.json") // It's safe to ignore gosec warning G304 since the file path suffix is hardcoded // nolint:gosec - data, err = os.ReadFile(pluginDataPath) + data, err = os.ReadFile(pluginPath) if err != nil { - return installedPlugin{}, fmt.Errorf("could not find dist/plugin.json or plugin.json for %s in %s", pluginID, pluginDir) + return plugins.JSONData{}, fmt.Errorf("could not find plugin.json or dist/plugin.json for %s in %s", pluginID, pluginDir) } } - res := installedPlugin{} - if err = json.Unmarshal(data, &res); err != nil { - return res, err + pJSON, err := plugins.ReadPluginJSON(bytes.NewReader(data)) + if err != nil { + return plugins.JSONData{}, err } - if res.ID == "" { - return installedPlugin{}, fmt.Errorf("could not find valid plugin %s in %s", pluginID, pluginDir) - } - - if res.Info.Version == "" { - res.Info.Version = "0.0.0" - } - - return res, nil + return pJSON, nil } diff --git a/pkg/plugins/storage/models.go b/pkg/plugins/storage/models.go index d758b18660f..6cc67e48ade 100644 --- a/pkg/plugins/storage/models.go +++ b/pkg/plugins/storage/models.go @@ -21,28 +21,3 @@ type Dependency struct { ID string Version string } - -type installedPlugin struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Info pluginInfo `json:"info"` - Dependencies dependencies `json:"dependencies"` -} - -type dependencies struct { - GrafanaVersion string `json:"grafanaVersion"` - Plugins []pluginDependency `json:"plugins"` -} - -type pluginDependency struct { - ID string `json:"id"` - Type string `json:"type"` - Name string `json:"name"` - Version string `json:"version"` -} - -type pluginInfo struct { - Version string `json:"version"` - Updated string `json:"updated"` -}