grafana/pkg/plugins/repo/service.go
Will Browne 26dfdd5af3
Plugins: Refactor plugin download/installation (#43046)
* installer -> repo

* add semver format checking

* add plugin callbacks in test

* remove newline

* post install only scans new directories

* remove unused stuff

* everything in own package

* add missing cli params

* make grafana version part of the API

* resolve conflicts

* tidy up logger

* fix cli and tidy log statements

* rename log package

* update struct name

* fix linter issue

* fs -> filestore

* reorder imports

* alias import

* fix test

* fix test

* inline var

* revert jsonc file

* make repo dep of manager

* actually inject the thing

* accept all args for compatability checks

* accept compat from store

* pass os + arch vals

* don't inject fs

* tidy up

* tidy up

* merge with main and tidy fs storage

* fix test

* fix packages

* fix comment + field name

* update fs naming

* fixed wire

* remove unused func

* fix mocks

* fix storage test

* renaming

* fix log line

* fix test

* re-order field

* tidying

* add test for update with same version

* fix wire for CLI

* remove use of ioutil

* don't pass field

* small tidy

* ignore code scanning warn

* fix testdata link

* update lgtm code
2022-08-23 11:50:50 +02:00

184 lines
4.9 KiB
Go

package repo
import (
"context"
"encoding/json"
"fmt"
"net/url"
"path"
"strings"
"github.com/grafana/grafana/pkg/plugins/logger"
)
type Manager struct {
client *Client
baseURL string
log logger.Logger
}
func ProvideService() *Manager {
defaultBaseURL := "https://grafana.com/api/plugins"
return New(false, defaultBaseURL, logger.NewLogger("plugin.repository"))
}
func New(skipTLSVerify bool, baseURL string, logger logger.Logger) *Manager {
return &Manager{
client: newClient(skipTLSVerify, logger),
baseURL: baseURL,
log: logger,
}
}
// GetPluginArchive fetches the requested plugin archive
func (m *Manager) GetPluginArchive(ctx context.Context, pluginID, version string, compatOpts CompatOpts) (*PluginArchive, error) {
dlOpts, err := m.GetPluginDownloadOptions(ctx, pluginID, version, compatOpts)
if err != nil {
return nil, err
}
return m.client.download(ctx, dlOpts.PluginZipURL, dlOpts.Checksum, compatOpts)
}
// GetPluginArchiveByURL fetches the requested plugin archive from the provided `pluginZipURL`
func (m *Manager) GetPluginArchiveByURL(ctx context.Context, pluginZipURL string, compatOpts CompatOpts) (*PluginArchive, error) {
return m.client.download(ctx, pluginZipURL, "", compatOpts)
}
// GetPluginDownloadOptions returns the options for downloading the requested plugin (with optional `version`)
func (m *Manager) GetPluginDownloadOptions(_ context.Context, pluginID, version string, compatOpts CompatOpts) (*PluginDownloadOptions, error) {
plugin, err := m.pluginMetadata(pluginID, compatOpts)
if err != nil {
return nil, err
}
v, err := m.selectVersion(&plugin, version, compatOpts)
if err != nil {
return nil, err
}
// Plugins which are downloaded just as sourcecode zipball from GitHub do not have checksum
var checksum string
if v.Arch != nil {
archMeta, exists := v.Arch[compatOpts.OSAndArch()]
if !exists {
archMeta = v.Arch["any"]
}
checksum = archMeta.SHA256
}
return &PluginDownloadOptions{
Version: v.Version,
Checksum: checksum,
PluginZipURL: fmt.Sprintf("%s/%s/versions/%s/download", m.baseURL, pluginID, v.Version),
}, nil
}
func (m *Manager) pluginMetadata(pluginID string, compatOpts CompatOpts) (Plugin, error) {
m.log.Debugf("Fetching metadata for plugin \"%s\" from repo %s", pluginID, m.baseURL)
u, err := url.Parse(m.baseURL)
if err != nil {
return Plugin{}, err
}
u.Path = path.Join(u.Path, "repo", pluginID)
body, err := m.client.sendReq(u, compatOpts)
if err != nil {
return Plugin{}, err
}
var data Plugin
err = json.Unmarshal(body, &data)
if err != nil {
m.log.Error("Failed to unmarshal plugin repo response error", err)
return Plugin{}, err
}
return data, nil
}
// selectVersion selects the most appropriate plugin version
// returns the specified version if supported.
// returns the latest version if no specific version is specified.
// returns error if the supplied version does not exist.
// returns error if supplied version exists but is not supported.
// NOTE: It expects plugin.Versions to be sorted so the newest version is first.
func (m *Manager) selectVersion(plugin *Plugin, version string, compatOpts CompatOpts) (*Version, error) {
version = normalizeVersion(version)
var ver Version
latestForArch := latestSupportedVersion(plugin, compatOpts)
if latestForArch == nil {
return nil, ErrVersionUnsupported{
PluginID: plugin.ID,
RequestedVersion: version,
SystemInfo: compatOpts.String(),
}
}
if version == "" {
return latestForArch, nil
}
for _, v := range plugin.Versions {
if v.Version == version {
ver = v
break
}
}
if len(ver.Version) == 0 {
m.log.Debugf("Requested plugin version %s v%s not found but potential fallback version '%s' was found",
plugin.ID, version, latestForArch.Version)
return nil, ErrVersionNotFound{
PluginID: plugin.ID,
RequestedVersion: version,
SystemInfo: compatOpts.String(),
}
}
if !supportsCurrentArch(&ver, compatOpts) {
m.log.Debugf("Requested plugin version %s v%s is not supported on your system but potential fallback version '%s' was found",
plugin.ID, version, latestForArch.Version)
return nil, ErrVersionUnsupported{
PluginID: plugin.ID,
RequestedVersion: version,
SystemInfo: compatOpts.String(),
}
}
return &ver, nil
}
func supportsCurrentArch(version *Version, compatOpts CompatOpts) bool {
if version.Arch == nil {
return true
}
for arch := range version.Arch {
if arch == compatOpts.OSAndArch() || arch == "any" {
return true
}
}
return false
}
func latestSupportedVersion(plugin *Plugin, compatOpts CompatOpts) *Version {
for _, v := range plugin.Versions {
ver := v
if supportsCurrentArch(&ver, compatOpts) {
return &ver
}
}
return nil
}
func normalizeVersion(version string) string {
normalized := strings.ReplaceAll(version, " ", "")
if strings.HasPrefix(normalized, "^") || strings.HasPrefix(normalized, "v") {
return normalized[1:]
}
return normalized
}