Plugins: Tidy up CLI code (#67813)

* more tidying

* move some things around

* more tidying

* fix linter
This commit is contained in:
Will Browne 2023-05-08 10:58:47 +02:00 committed by GitHub
parent 0fc9a47779
commit e0e2535c96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 307 additions and 298 deletions

View File

@ -127,7 +127,7 @@ func osAndArchString() string {
return osString + "-" + arch return osString + "-" + arch
} }
func supportsCurrentArch(version *models.Version) bool { func supportsCurrentArch(version models.Version) bool {
if version.Arch == nil { if version.Arch == nil {
return true return true
} }
@ -139,10 +139,10 @@ func supportsCurrentArch(version *models.Version) bool {
return false return false
} }
func latestSupportedVersion(plugin *models.Plugin) *models.Version { func latestSupportedVersion(plugin models.Plugin) *models.Version {
for _, v := range plugin.Versions { for _, v := range plugin.Versions {
ver := v ver := v
if supportsCurrentArch(&ver) { if supportsCurrentArch(ver) {
return &ver return &ver
} }
} }

View File

@ -15,11 +15,10 @@ func listRemoteCommand(c utils.CommandLine) error {
} }
for _, p := range plugin.Plugins { for _, p := range plugin.Plugins {
plugin := p if len(p.Versions) > 0 {
if len(plugin.Versions) > 0 { ver := latestSupportedVersion(p)
ver := latestSupportedVersion(&plugin)
if ver != nil { 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)
} }
} }
} }

View File

@ -24,7 +24,7 @@ func listVersionsCommand(c utils.CommandLine) error {
pluginToList := c.Args().First() pluginToList := c.Args().First()
plugin, err := services.GetPlugin(pluginToList, c.String("repo")) plugin, err := services.GetPluginInfoFromRepo(pluginToList, c.String("repo"))
if err != nil { if err != nil {
return err return err
} }

View File

@ -6,13 +6,10 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "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/services"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
) )
var ls_getPlugins func(path string) []models.InstalledPlugin = services.GetLocalPlugins
var ( var (
errMissingPathFlag = errors.New("missing path flag") errMissingPathFlag = errors.New("missing path flag")
errNotDirectory = errors.New("plugin path is not a directory") errNotDirectory = errors.New("plugin path is not a directory")
@ -41,7 +38,7 @@ func lsCommand(c utils.CommandLine) error {
return err return err
} }
plugins := ls_getPlugins(pluginDir) plugins := services.GetLocalPlugins(pluginDir)
if len(plugins) > 0 { if len(plugins) > 0 {
logger.Info("installed plugins:\n") logger.Info("installed plugins:\n")
@ -50,7 +47,8 @@ func lsCommand(c utils.CommandLine) error {
} }
for _, plugin := range plugins { 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 return nil

View File

@ -9,10 +9,15 @@ import (
"github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "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/services"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/plugins"
) )
func shouldUpgrade(installed string, remote *models.Plugin) bool { func shouldUpgrade(installed plugins.FoundPlugin, remote models.Plugin) bool {
installedVersion, err := version.NewVersion(installed) installedVer := installed.JSONData.Info.Version
if installedVer == "" {
installedVer = "0.0.0"
}
installedVersion, err := version.NewVersion(installedVer)
if err != nil { if err != nil {
return false return false
} }
@ -35,30 +40,30 @@ func upgradeAllCommand(c utils.CommandLine) error {
return err return err
} }
pluginsToUpgrade := make([]models.InstalledPlugin, 0) pluginsToUpgrade := make([]plugins.FoundPlugin, 0)
for _, localPlugin := range localPlugins { for _, localPlugin := range localPlugins {
for _, p := range remotePlugins.Plugins { for _, p := range remotePlugins.Plugins {
remotePlugin := p remotePlugin := p
if localPlugin.ID != remotePlugin.ID { if localPlugin.Primary.JSONData.ID != remotePlugin.ID {
continue continue
} }
if shouldUpgrade(localPlugin.Info.Version, &remotePlugin) { if shouldUpgrade(localPlugin.Primary, remotePlugin) {
pluginsToUpgrade = append(pluginsToUpgrade, localPlugin) pluginsToUpgrade = append(pluginsToUpgrade, localPlugin.Primary)
} }
} }
} }
ctx := context.Background() ctx := context.Background()
for _, p := range pluginsToUpgrade { 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 { if err != nil {
return err return err
} }
err = installPlugin(ctx, p.ID, "", c) err = installPlugin(ctx, p.JSONData.ID, "", c)
if err != nil { if err != nil {
return err return err
} }

View File

@ -4,9 +4,10 @@ import (
"fmt" "fmt"
"testing" "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/cmd/grafana-cli/models"
"github.com/grafana/grafana/pkg/plugins"
) )
func TestVersionComparison(t *testing.T) { func TestVersionComparison(t *testing.T) {
@ -16,15 +17,23 @@ func TestVersionComparison(t *testing.T) {
{Version: "2.0.0"}, {Version: "2.0.0"},
} }
upgradeablePlugins := map[string]models.Plugin{ upgradeablePlugins := []struct {
"0.0.0": {Versions: versions}, have plugins.FoundPlugin
"1.0.0": {Versions: versions}, 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 { for _, v := range upgradeablePlugins {
val := v t.Run(fmt.Sprintf("for %s should be true", v.have.JSONData.Info.Version), func(t *testing.T) {
t.Run(fmt.Sprintf("for %s should be true", k), func(t *testing.T) { require.True(t, shouldUpgrade(v.have, v.requested))
assert.True(t, shouldUpgrade(k, &val))
}) })
} }
}) })
@ -35,15 +44,23 @@ func TestVersionComparison(t *testing.T) {
{Version: "2.0.0"}, {Version: "2.0.0"},
} }
shouldNotUpgrade := map[string]models.Plugin{ shouldNotUpgrade := []struct {
"2.0.0": {Versions: versions}, have plugins.FoundPlugin
"6.0.0": {Versions: versions}, 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 { for _, v := range shouldNotUpgrade {
val := v t.Run(fmt.Sprintf("for %s should be false", v.have.JSONData.Info.Version), func(t *testing.T) {
t.Run(fmt.Sprintf("for %s should be false", k), func(t *testing.T) { require.False(t, shouldUpgrade(v.have, v.requested))
assert.False(t, shouldUpgrade(k, &val))
}) })
} }
}) })

View File

@ -16,17 +16,17 @@ func upgradeCommand(c utils.CommandLine) error {
pluginsDir := c.PluginDirectory() pluginsDir := c.PluginDirectory()
pluginID := c.Args().First() pluginID := c.Args().First()
localPlugin, err := services.ReadPlugin(pluginsDir, pluginID) localPlugin, err := services.GetLocalPlugin(pluginsDir, pluginID)
if err != nil { if err != nil {
return err return err
} }
plugin, err := services.GetPlugin(pluginID, c.PluginRepoURL()) plugin, err := services.GetPluginInfoFromRepo(pluginID, c.PluginRepoURL())
if err != nil { if err != nil {
return err return err
} }
if shouldUpgrade(localPlugin.Info.Version, &plugin) { if shouldUpgrade(localPlugin, plugin) {
if err = uninstallPlugin(ctx, pluginID, c); err != nil { if err = uninstallPlugin(ctx, pluginID, c); err != nil {
return fmt.Errorf("failed to remove plugin '%s': %w", pluginID, err) return fmt.Errorf("failed to remove plugin '%s': %w", pluginID, err)
} }

View File

@ -14,7 +14,7 @@ import (
"github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "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) logger.Debugf("getting plugin metadata from: %v pluginId: %v \n", repoUrl, pluginId)
body, err := sendRequestGetBytes(HttpClient, repoUrl, "repo", pluginId) body, err := sendRequestGetBytes(HttpClient, repoUrl, "repo", pluginId)
if err != nil { if err != nil {

View File

@ -1,8 +1,8 @@
package services package services
import ( import (
"context"
"crypto/tls" "crypto/tls"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -12,6 +12,10 @@ import (
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "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/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 ( var (
@ -62,43 +66,25 @@ func makeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client {
} }
} }
func ReadPlugin(pluginDir, pluginName string) (models.InstalledPlugin, error) { func GetLocalPlugin(pluginDir, pluginID string) (plugins.FoundPlugin, error) {
distPluginDataPath := filepath.Join(pluginDir, pluginName, "dist", "plugin.json") 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 { if err != nil {
pluginDataPath := filepath.Join(pluginDir, pluginName, "plugin.json") logger.Error("Could not get local plugins", err)
data, err = IoHelper.ReadFile(pluginDataPath) return make([]*plugins.FoundBundle, 0)
if err != nil {
return models.InstalledPlugin{}, errors.New("Could not find dist/plugin.json or plugin.json for " + pluginName + " in " + pluginDir)
}
} }
res := models.InstalledPlugin{} return res
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
} }

View File

@ -20,7 +20,6 @@ import (
var walk = util.Walk var walk = util.Walk
var ( var (
ErrInvalidPluginJSON = errors.New("did not find valid type or id properties in plugin.json")
ErrInvalidPluginJSONFilePath = errors.New("invalid plugin.json filepath was provided") 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) l.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "err", err)
return plugins.JSONData{}, err return plugins.JSONData{}, err
} }
plugin, err := ReadPluginJSON(reader) plugin, err := plugins.ReadPluginJSON(reader)
if err != nil { if err != nil {
l.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "err", err) l.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "err", err)
return plugins.JSONData{}, err return plugins.JSONData{}, err

View File

@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/services/featuremgmt" "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/services/pluginsintegration/config"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "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 { var fsComparer = cmp.Comparer(func(fs1 plugins.FS, fs2 plugins.FS) bool {
fs1Files, err := fs1.Files() fs1Files, err := fs1.Files()
if err != nil { if err != nil {

View File

@ -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
}

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"io/fs" "io/fs"
"path" "path"
"runtime" "runtime"
@ -24,6 +25,7 @@ var (
ErrFileNotExist = errors.New("file does not exist") ErrFileNotExist = errors.New("file does not exist")
ErrPluginFileRead = errors.New("file could not be read") ErrPluginFileRead = errors.New("file could not be read")
ErrUninstallInvalidPluginDir = errors.New("cannot recognize as plugin folder") 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 { type Plugin struct {
@ -139,6 +141,44 @@ type JSONData struct {
Executable string `json:"executable,omitempty"` 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 { func (d JSONData) DashboardIncludes() []*Includes {
result := []*Includes{} result := []*Includes{}
for _, include := range d.Includes { for _, include := range d.Includes {

167
pkg/plugins/plugins_test.go Normal file
View File

@ -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)
}
})
}
}

View File

@ -4,7 +4,6 @@ import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -13,6 +12,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/log" "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) 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 { if err != nil {
return nil, fmt.Errorf("%v: %w", "failed to convert to plugin DTO", err) 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)) deps := make([]*Dependency, 0, len(pluginJSON.Dependencies.Plugins))
for _, plugin := range res.Dependencies.Plugins { for _, plugin := range pluginJSON.Dependencies.Plugins {
deps = append(deps, &Dependency{ deps = append(deps, &Dependency{
ID: plugin.ID, ID: plugin.ID,
Version: plugin.Version, Version: plugin.Version,
@ -55,8 +55,8 @@ func (fs *FS) Extract(ctx context.Context, pluginID string, pluginArchive *zip.R
} }
return &ExtractedPluginArchive{ return &ExtractedPluginArchive{
ID: res.ID, ID: pluginJSON.ID,
Version: res.Info.Version, Version: pluginJSON.Info.Version,
Dependencies: deps, Dependencies: deps,
Path: pluginDir, Path: pluginDir,
}, nil }, nil
@ -220,34 +220,26 @@ func removeGitBuildFromName(filename, pluginID string) string {
return reGitBuild.ReplaceAllString(filename, pluginID+"/") return reGitBuild.ReplaceAllString(filename, pluginID+"/")
} }
func toPluginDTO(pluginID, pluginDir string) (installedPlugin, error) { func readPluginJSON(pluginID, pluginDir string) (plugins.JSONData, error) {
distPluginDataPath := filepath.Join(pluginDir, "dist", "plugin.json") pluginPath := filepath.Join(pluginDir, "plugin.json")
// It's safe to ignore gosec warning G304 since the file path suffix is hardcoded // It's safe to ignore gosec warning G304 since the file path suffix is hardcoded
// nolint:gosec // nolint:gosec
data, err := os.ReadFile(distPluginDataPath) data, err := os.ReadFile(pluginPath)
if err != nil { 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 // It's safe to ignore gosec warning G304 since the file path suffix is hardcoded
// nolint:gosec // nolint:gosec
data, err = os.ReadFile(pluginDataPath) data, err = os.ReadFile(pluginPath)
if err != nil { 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{} pJSON, err := plugins.ReadPluginJSON(bytes.NewReader(data))
if err = json.Unmarshal(data, &res); err != nil { if err != nil {
return res, err return plugins.JSONData{}, err
} }
if res.ID == "" { return pJSON, nil
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
} }

View File

@ -21,28 +21,3 @@ type Dependency struct {
ID string ID string
Version 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"`
}