mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Move plugin installing + uninstalling logic from CLI to plugins package (#33274)
* move guts from cli to server * renaming + refactoring * add pluginsDir arg * arg fixes * add support for repo URL override * add funcs to interface * use pluginID consistently * swap args * pass mandatory grafanaVersion field * introduce logger interface * create central logger for CLI * add infra log wrapper * re-add log initer step * remove unused logger * add checks for uninstalling * improve debug blue * make sure to close file * fix linter issues * remove space * improve newline usage * refactor packaging * improve logger API * fix interface func names * close file and reformat zipslip catch * handle G305 linter warning * add helpful debug log
This commit is contained in:
@@ -3,6 +3,8 @@ package commands
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/datamigrations"
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/datamigrations"
|
||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||||
@@ -57,7 +59,7 @@ func runPluginCommand(command func(commandLine utils.CommandLine) error) func(co
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("\nRestart Grafana after installing plugins. Refer to Grafana documentation for instructions if necessary.\n\n\n\n")
|
logger.Info(color.GreenString("Please restart Grafana after installing plugins. Refer to Grafana documentation for instructions if necessary.\n\n"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
"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/cmd/grafana-cli/utils"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/installer"
|
||||||
"github.com/grafana/grafana/pkg/util/errutil"
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
|
||||||
"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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func validateInput(c utils.CommandLine, pluginFolder string) error {
|
func validateInput(c utils.CommandLine, pluginFolder string) error {
|
||||||
@@ -54,10 +55,12 @@ func (cmd Command) installCommand(c utils.CommandLine) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginToInstall := c.Args().First()
|
pluginID := c.Args().First()
|
||||||
version := c.Args().Get(1)
|
version := c.Args().Get(1)
|
||||||
|
skipTLSVerify := c.Bool("insecure")
|
||||||
|
|
||||||
return InstallPlugin(pluginToInstall, version, c, cmd.Client)
|
i := installer.New(skipTLSVerify, services.GrafanaVersion, services.Logger)
|
||||||
|
return i.Install(pluginID, version, c.PluginDirectory(), c.PluginURL(), c.PluginRepoURL())
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallPlugin downloads the plugin code as a zip file from the Grafana.com API
|
// InstallPlugin downloads the plugin code as a zip file from the Grafana.com API
|
||||||
@@ -76,7 +79,7 @@ func InstallPlugin(pluginName, version string, c utils.CommandLine, client utils
|
|||||||
// is up to the user to know what she is doing.
|
// is up to the user to know what she is doing.
|
||||||
isInternal = true
|
isInternal = true
|
||||||
}
|
}
|
||||||
plugin, err := client.GetPlugin(pluginName, c.RepoDirectory())
|
plugin, err := client.GetPlugin(pluginName, c.PluginRepoURL())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
// listRemoteCommand prints out all plugins in the remote repo with latest version supported on current platform.
|
// listRemoteCommand prints out all plugins in the remote repo with latest version supported on current platform.
|
||||||
// If there are no supported versions for plugin it is skipped.
|
// If there are no supported versions for plugin it is skipped.
|
||||||
func (cmd Command) listRemoteCommand(c utils.CommandLine) error {
|
func (cmd Command) listRemoteCommand(c utils.CommandLine) error {
|
||||||
plugin, err := cmd.Client.ListAllPlugins(c.RepoDirectory())
|
plugin, err := cmd.Client.ListAllPlugins(c.PluginRepoURL())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func (cmd Command) upgradeCommand(c utils.CommandLine) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin, err2 := cmd.Client.GetPlugin(pluginName, c.RepoDirectory())
|
plugin, err2 := cmd.Client.GetPlugin(pluginName, c.PluginRepoURL())
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
return err2
|
return err2
|
||||||
}
|
}
|
||||||
|
|||||||
74
pkg/cmd/grafana-cli/logger/loggerV2.go
Normal file
74
pkg/cmd/grafana-cli/logger/loggerV2.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CLILogger struct {
|
||||||
|
DebugMode bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(debugMode bool) *CLILogger {
|
||||||
|
return &CLILogger{
|
||||||
|
DebugMode: debugMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CLILogger) Successf(format string, args ...interface{}) {
|
||||||
|
fmt.Printf(fmt.Sprintf("%s %s\n\n", color.GreenString("✔"), format), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CLILogger) Failuref(format string, args ...interface{}) {
|
||||||
|
fmt.Printf(fmt.Sprintf("%s %s %s\n\n", color.RedString("Error"), color.RedString("✗"), format), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CLILogger) Info(args ...interface{}) {
|
||||||
|
args = append(args, "\n\n")
|
||||||
|
fmt.Print(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CLILogger) Infof(format string, args ...interface{}) {
|
||||||
|
fmt.Printf(addNewlines(format), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CLILogger) Debug(args ...interface{}) {
|
||||||
|
args = append(args, "\n\n")
|
||||||
|
if l.DebugMode {
|
||||||
|
fmt.Print(color.HiBlueString(fmt.Sprint(args...)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CLILogger) Debugf(format string, args ...interface{}) {
|
||||||
|
if l.DebugMode {
|
||||||
|
fmt.Print(color.HiBlueString(fmt.Sprintf(addNewlines(format), args...)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CLILogger) Warn(args ...interface{}) {
|
||||||
|
args = append(args, "\n\n")
|
||||||
|
fmt.Print(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CLILogger) Warnf(format string, args ...interface{}) {
|
||||||
|
fmt.Printf(addNewlines(format), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CLILogger) Error(args ...interface{}) {
|
||||||
|
args = append(args, "\n\n")
|
||||||
|
fmt.Print(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CLILogger) Errorf(format string, args ...interface{}) {
|
||||||
|
fmt.Printf(addNewlines(format), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNewlines(str string) string {
|
||||||
|
var s strings.Builder
|
||||||
|
s.WriteString(str)
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.Before = func(c *cli.Context) error {
|
app.Before = func(c *cli.Context) error {
|
||||||
services.Init(version, c.Bool("insecure"))
|
services.Init(version, c.Bool("insecure"), c.Bool("debug"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,10 +174,10 @@ func createRequest(repoUrl string, subPaths ...string) (*http.Request, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("grafana-version", grafanaVersion)
|
req.Header.Set("grafana-version", GrafanaVersion)
|
||||||
req.Header.Set("grafana-os", runtime.GOOS)
|
req.Header.Set("grafana-os", runtime.GOOS)
|
||||||
req.Header.Set("grafana-arch", runtime.GOARCH)
|
req.Header.Set("grafana-arch", runtime.GOARCH)
|
||||||
req.Header.Set("User-Agent", "grafana "+grafanaVersion)
|
req.Header.Set("User-Agent", "grafana "+GrafanaVersion)
|
||||||
|
|
||||||
return req, err
|
return req, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ var (
|
|||||||
IoHelper models.IoUtil = IoUtilImp{}
|
IoHelper models.IoUtil = IoUtilImp{}
|
||||||
HttpClient http.Client
|
HttpClient http.Client
|
||||||
HttpClientNoTimeout http.Client
|
HttpClientNoTimeout http.Client
|
||||||
grafanaVersion string
|
GrafanaVersion string
|
||||||
ErrNotFoundError = errors.New("404 not found error")
|
ErrNotFoundError = errors.New("404 not found error")
|
||||||
|
Logger *logger.CLILogger
|
||||||
)
|
)
|
||||||
|
|
||||||
type BadRequestError struct {
|
type BadRequestError struct {
|
||||||
@@ -34,11 +35,12 @@ func (e *BadRequestError) Error() string {
|
|||||||
return e.Status
|
return e.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func Init(version string, skipTLSVerify bool) {
|
func Init(version string, skipTLSVerify bool, debugMode bool) {
|
||||||
grafanaVersion = version
|
GrafanaVersion = version
|
||||||
|
|
||||||
HttpClient = makeHttpClient(skipTLSVerify, 10*time.Second)
|
HttpClient = makeHttpClient(skipTLSVerify, 10*time.Second)
|
||||||
HttpClientNoTimeout = makeHttpClient(skipTLSVerify, 0)
|
HttpClientNoTimeout = makeHttpClient(skipTLSVerify, 0)
|
||||||
|
Logger = logger.New(debugMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client {
|
func makeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client {
|
||||||
@@ -113,5 +115,6 @@ func RemoveInstalledPlugin(pluginPath, pluginName string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Removing directory %v\n", pluginDir)
|
||||||
return IoHelper.RemoveAll(pluginDir)
|
return IoHelper.RemoveAll(pluginDir)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type CommandLine interface {
|
|||||||
Generic(name string) interface{}
|
Generic(name string) interface{}
|
||||||
|
|
||||||
PluginDirectory() string
|
PluginDirectory() string
|
||||||
RepoDirectory() string
|
PluginRepoURL() string
|
||||||
PluginURL() string
|
PluginURL() string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ func (c *ContextCommandLine) PluginDirectory() string {
|
|||||||
return c.String("pluginsDir")
|
return c.String("pluginsDir")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ContextCommandLine) RepoDirectory() string {
|
func (c *ContextCommandLine) PluginRepoURL() string {
|
||||||
return c.String("repo")
|
return c.String("repo")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package plugins
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
@@ -70,3 +71,26 @@ type DataRequestHandler interface {
|
|||||||
// HandleRequest handles a data request.
|
// HandleRequest handles a data request.
|
||||||
HandleRequest(context.Context, *models.DataSource, DataQuery) (DataResponse, error)
|
HandleRequest(context.Context, *models.DataSource, DataQuery) (DataResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PluginInstaller interface {
|
||||||
|
// Install finds the plugin given the provided information
|
||||||
|
// and installs in the provided plugins directory.
|
||||||
|
Install(pluginID, version, pluginsDirectory, pluginZipURL, pluginRepoURL string) error
|
||||||
|
// Uninstall removes the specified plugin from the provided plugins directory.
|
||||||
|
Uninstall(pluginID, pluginPath string) error
|
||||||
|
DownloadFile(pluginID string, tmpFile *os.File, url string, checksum string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginInstallerLogger interface {
|
||||||
|
Successf(format string, args ...interface{})
|
||||||
|
Failuref(format string, args ...interface{})
|
||||||
|
|
||||||
|
Info(args ...interface{})
|
||||||
|
Infof(format string, args ...interface{})
|
||||||
|
Debug(args ...interface{})
|
||||||
|
Debugf(format string, args ...interface{})
|
||||||
|
Warn(args ...interface{})
|
||||||
|
Warnf(format string, args ...interface{})
|
||||||
|
Error(args ...interface{})
|
||||||
|
Errorf(format string, args ...interface{})
|
||||||
|
}
|
||||||
|
|||||||
640
pkg/plugins/manager/installer/installer.go
Normal file
640
pkg/plugins/manager/installer/installer.go
Normal file
@@ -0,0 +1,640 @@
|
|||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Installer struct {
|
||||||
|
retryCount int
|
||||||
|
|
||||||
|
httpClient http.Client
|
||||||
|
httpClientNoTimeout http.Client
|
||||||
|
grafanaVersion string
|
||||||
|
log plugins.PluginInstallerLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
permissionsDeniedMessage = "could not create %q, permission denied, make sure you have write access to plugin dir"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFoundError = errors.New("404 not found error")
|
||||||
|
reGitBuild = regexp.MustCompile("^[a-zA-Z0-9_.-]*/")
|
||||||
|
)
|
||||||
|
|
||||||
|
type BadRequestError struct {
|
||||||
|
Message string
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *BadRequestError) Error() string {
|
||||||
|
if len(e.Message) > 0 {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Status, e.Message)
|
||||||
|
}
|
||||||
|
return e.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(skipTLSVerify bool, grafanaVersion string, logger plugins.PluginInstallerLogger) *Installer {
|
||||||
|
return &Installer{
|
||||||
|
httpClient: makeHttpClient(skipTLSVerify, 10*time.Second),
|
||||||
|
httpClientNoTimeout: makeHttpClient(skipTLSVerify, 10*time.Second),
|
||||||
|
log: logger,
|
||||||
|
grafanaVersion: grafanaVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install downloads the plugin code as a zip file from specified URL
|
||||||
|
// and then extracts the zip into the provided plugins directory.
|
||||||
|
func (i *Installer) Install(pluginID, version, pluginsDir, pluginZipURL, pluginRepoURL string) error {
|
||||||
|
isInternal := false
|
||||||
|
|
||||||
|
var checksum string
|
||||||
|
if pluginZipURL == "" {
|
||||||
|
if strings.HasPrefix(pluginID, "grafana-") {
|
||||||
|
// At this point the plugin download is going through grafana.com API and thus the name is validated.
|
||||||
|
// Checking for grafana prefix is how it is done there so no 3rd party plugin should have that prefix.
|
||||||
|
// You can supply custom plugin name and then set custom download url to 3rd party plugin but then that
|
||||||
|
// is up to the user to know what she is doing.
|
||||||
|
isInternal = true
|
||||||
|
}
|
||||||
|
plugin, err := i.getPluginMetadataFromPluginRepo(pluginID, pluginRepoURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := selectVersion(&plugin, version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if version == "" {
|
||||||
|
version = v.Version
|
||||||
|
}
|
||||||
|
pluginZipURL = fmt.Sprintf("%s/%s/versions/%s/download",
|
||||||
|
pluginRepoURL,
|
||||||
|
pluginID,
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Plugins which are downloaded just as sourcecode zipball from github do not have checksum
|
||||||
|
if v.Arch != nil {
|
||||||
|
archMeta, exists := v.Arch[osAndArchString()]
|
||||||
|
if !exists {
|
||||||
|
archMeta = v.Arch["any"]
|
||||||
|
}
|
||||||
|
checksum = archMeta.SHA256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i.log.Debugf("Installing plugin\nfrom: %s\ninto: %s", pluginZipURL, pluginsDir)
|
||||||
|
|
||||||
|
// Create temp file for downloading zip file
|
||||||
|
tmpFile, err := ioutil.TempFile("", "*.zip")
|
||||||
|
if err != nil {
|
||||||
|
return errutil.Wrap("failed to create temporary file", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := os.Remove(tmpFile.Name()); err != nil {
|
||||||
|
i.log.Warn("Failed to remove temporary file", "file", tmpFile.Name(), "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = i.DownloadFile(pluginID, tmpFile, pluginZipURL, checksum)
|
||||||
|
if err != nil {
|
||||||
|
if err := tmpFile.Close(); err != nil {
|
||||||
|
i.log.Warn("Failed to close file", "err", err)
|
||||||
|
}
|
||||||
|
return errutil.Wrap("failed to download plugin archive", err)
|
||||||
|
}
|
||||||
|
err = tmpFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errutil.Wrap("failed to close tmp file", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.extractFiles(tmpFile.Name(), pluginID, pluginsDir, isInternal)
|
||||||
|
if err != nil {
|
||||||
|
return errutil.Wrap("failed to extract plugin archive", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, _ := toPluginDTO(pluginsDir, pluginID)
|
||||||
|
|
||||||
|
i.log.Successf("Installed %s v%s successfully", res.ID, res.Info.Version)
|
||||||
|
|
||||||
|
// download dependency plugins
|
||||||
|
for _, dep := range res.Dependencies.Plugins {
|
||||||
|
i.log.Infof("Fetching %s dependencies...", res.ID)
|
||||||
|
if err := i.Install(dep.ID, normalizeVersion(dep.Version), pluginsDir, "", pluginRepoURL); err != nil {
|
||||||
|
return errutil.Wrapf(err, "failed to install plugin '%s'", dep.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uninstall removes the specified plugin from the provided plugins directory.
|
||||||
|
func (i *Installer) Uninstall(pluginID, pluginPath string) error {
|
||||||
|
pluginDir := filepath.Join(pluginPath, pluginID)
|
||||||
|
|
||||||
|
// verify it's a plugin directory
|
||||||
|
if _, err := os.Stat(filepath.Join(pluginDir, "plugin.json")); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if _, err := os.Stat(filepath.Join(pluginDir, "dist", "plugin.json")); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("tried to remove %s, but it doesn't seem to be a plugin", pluginPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i.log.Infof("Uninstalling plugin %v", pluginID)
|
||||||
|
|
||||||
|
return os.RemoveAll(pluginDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Installer) DownloadFile(pluginID string, tmpFile *os.File, url string, checksum string) (err error) {
|
||||||
|
// Try handling URL as a local file path first
|
||||||
|
if _, err := os.Stat(url); err == nil {
|
||||||
|
// We can ignore this gosec G304 warning since `url` stems from command line flag "pluginUrl". If the
|
||||||
|
// user shouldn't be able to read the file, it should be handled through filesystem permissions.
|
||||||
|
// nolint:gosec
|
||||||
|
f, err := os.Open(url)
|
||||||
|
if err != nil {
|
||||||
|
return errutil.Wrap("Failed to read plugin archive", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
i.log.Warn("Failed to close file", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, err = io.Copy(tmpFile, f)
|
||||||
|
if err != nil {
|
||||||
|
return errutil.Wrap("Failed to copy plugin archive", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
i.retryCount = 0
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
i.retryCount++
|
||||||
|
if i.retryCount < 3 {
|
||||||
|
i.log.Debug("Failed downloading. Will retry once.")
|
||||||
|
err = tmpFile.Truncate(0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = tmpFile.Seek(0, 0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = i.DownloadFile(pluginID, tmpFile, url, checksum)
|
||||||
|
} else {
|
||||||
|
i.retryCount = 0
|
||||||
|
failure := fmt.Sprintf("%v", r)
|
||||||
|
if failure == "runtime error: makeslice: len out of range" {
|
||||||
|
err = fmt.Errorf("corrupt HTTP response from source, please try again")
|
||||||
|
} else {
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Using no timeout here as some plugins can be bigger and smaller timeout would prevent to download a plugin on
|
||||||
|
// slow network. As this is CLI operation hanging is not a big of an issue as user can just abort.
|
||||||
|
bodyReader, err := i.sendRequestWithoutTimeout(url)
|
||||||
|
if err != nil {
|
||||||
|
return errutil.Wrap("Failed to send request", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := bodyReader.Close(); err != nil {
|
||||||
|
i.log.Warn("Failed to close body", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
w := bufio.NewWriter(tmpFile)
|
||||||
|
h := sha256.New()
|
||||||
|
if _, err = io.Copy(w, io.TeeReader(bodyReader, h)); err != nil {
|
||||||
|
return errutil.Wrap("failed to compute SHA256 checksum", err)
|
||||||
|
}
|
||||||
|
if err := w.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("failed to write to %q: %w", tmpFile.Name(), err)
|
||||||
|
}
|
||||||
|
if len(checksum) > 0 && checksum != fmt.Sprintf("%x", h.Sum(nil)) {
|
||||||
|
return fmt.Errorf("expected SHA256 checksum does not match the downloaded archive - please contact security@grafana.com")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Installer) getPluginMetadataFromPluginRepo(pluginID, pluginRepoURL string) (Plugin, error) {
|
||||||
|
i.log.Debugf("Fetching metadata for plugin \"%s\" from repo %s", pluginID, pluginRepoURL)
|
||||||
|
body, err := i.sendRequestGetBytes(pluginRepoURL, "repo", pluginID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrNotFoundError) {
|
||||||
|
return Plugin{},
|
||||||
|
fmt.Errorf("failed to find plugin \"%s\" in plugin repository. Please check if plugin ID is correct",
|
||||||
|
pluginID)
|
||||||
|
}
|
||||||
|
return Plugin{}, errutil.Wrap("Failed to send request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data Plugin
|
||||||
|
err = json.Unmarshal(body, &data)
|
||||||
|
if err != nil {
|
||||||
|
i.log.Error("Failed to unmarshal plugin repo response error", err)
|
||||||
|
return Plugin{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Installer) sendRequestGetBytes(URL string, subPaths ...string) ([]byte, error) {
|
||||||
|
bodyReader, err := i.sendRequest(URL, subPaths...)
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := bodyReader.Close(); err != nil {
|
||||||
|
i.log.Warn("Failed to close stream", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return ioutil.ReadAll(bodyReader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Installer) sendRequest(URL string, subPaths ...string) (io.ReadCloser, error) {
|
||||||
|
req, err := i.createRequest(URL, subPaths...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := i.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return i.handleResponse(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Installer) sendRequestWithoutTimeout(URL string, subPaths ...string) (io.ReadCloser, error) {
|
||||||
|
req, err := i.createRequest(URL, subPaths...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := i.httpClientNoTimeout.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return i.handleResponse(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Installer) createRequest(URL string, subPaths ...string) (*http.Request, error) {
|
||||||
|
u, err := url.Parse(URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range subPaths {
|
||||||
|
u.Path = path.Join(u.Path, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("grafana-version", i.grafanaVersion)
|
||||||
|
req.Header.Set("grafana-os", runtime.GOOS)
|
||||||
|
req.Header.Set("grafana-arch", runtime.GOARCH)
|
||||||
|
req.Header.Set("User-Agent", "grafana "+i.grafanaVersion)
|
||||||
|
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Installer) handleResponse(res *http.Response) (io.ReadCloser, error) {
|
||||||
|
if res.StatusCode == 404 {
|
||||||
|
return nil, ErrNotFoundError
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode/100 != 2 && res.StatusCode/100 != 4 {
|
||||||
|
return nil, fmt.Errorf("API returned invalid status: %s", res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode/100 == 4 {
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
defer func() {
|
||||||
|
if err := res.Body.Close(); err != nil {
|
||||||
|
i.log.Warn("Failed to close response body", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil || len(body) == 0 {
|
||||||
|
return nil, &BadRequestError{Status: res.Status}
|
||||||
|
}
|
||||||
|
var message string
|
||||||
|
var jsonBody map[string]string
|
||||||
|
err = json.Unmarshal(body, &jsonBody)
|
||||||
|
if err != nil || len(jsonBody["message"]) == 0 {
|
||||||
|
message = string(body)
|
||||||
|
} else {
|
||||||
|
message = jsonBody["message"]
|
||||||
|
}
|
||||||
|
return nil, &BadRequestError{Status: res.Status, Message: message}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client {
|
||||||
|
tr := &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: skipTLSVerify,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
Transport: tr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeVersion(version string) string {
|
||||||
|
normalized := strings.ReplaceAll(version, " ", "")
|
||||||
|
if strings.HasPrefix(normalized, "^") || strings.HasPrefix(normalized, "v") {
|
||||||
|
return normalized[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectVersion returns latest version if none is specified or the specified version. If the version string is not
|
||||||
|
// matched to existing version it errors out. It also errors out if version that is matched is not available for current
|
||||||
|
// os and platform. It expects plugin.Versions to be sorted so the newest version is first.
|
||||||
|
func selectVersion(plugin *Plugin, version string) (*Version, error) {
|
||||||
|
var ver Version
|
||||||
|
|
||||||
|
latestForArch := latestSupportedVersion(plugin)
|
||||||
|
if latestForArch == nil {
|
||||||
|
return nil, fmt.Errorf("%s is not supported on your architecture and OS", plugin.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if version == "" {
|
||||||
|
return latestForArch, nil
|
||||||
|
}
|
||||||
|
for _, v := range plugin.Versions {
|
||||||
|
if v.Version == version {
|
||||||
|
ver = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ver.Version) == 0 {
|
||||||
|
return nil, fmt.Errorf("could not find a version %s for %s. The latest suitable version is %s",
|
||||||
|
version, plugin.ID, latestForArch.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !supportsCurrentArch(&ver) {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"the version you requested is not supported on your architecture and OS, latest suitable version is %s",
|
||||||
|
latestForArch.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func osAndArchString() string {
|
||||||
|
osString := strings.ToLower(runtime.GOOS)
|
||||||
|
arch := runtime.GOARCH
|
||||||
|
return osString + "-" + arch
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportsCurrentArch(version *Version) bool {
|
||||||
|
if version.Arch == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for arch := range version.Arch {
|
||||||
|
if arch == osAndArchString() || arch == "any" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func latestSupportedVersion(plugin *Plugin) *Version {
|
||||||
|
for _, v := range plugin.Versions {
|
||||||
|
ver := v
|
||||||
|
if supportsCurrentArch(&ver) {
|
||||||
|
return &ver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Installer) extractFiles(archiveFile string, pluginID string, dest string, allowSymlinks bool) error {
|
||||||
|
var err error
|
||||||
|
dest, err = filepath.Abs(dest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.log.Debug(fmt.Sprintf("Extracting archive %q to %q...", archiveFile, dest))
|
||||||
|
|
||||||
|
existingInstallDir := filepath.Join(dest, pluginID)
|
||||||
|
if _, err := os.Stat(existingInstallDir); !os.IsNotExist(err) {
|
||||||
|
i.log.Debugf("Removing existing installation of plugin %s", existingInstallDir)
|
||||||
|
err = os.RemoveAll(existingInstallDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := zip.OpenReader(archiveFile)
|
||||||
|
defer func() {
|
||||||
|
if err := r.Close(); err != nil {
|
||||||
|
i.log.Warn("failed to close zip file", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, zf := range r.File {
|
||||||
|
// We can ignore gosec G305 here since we check for the ZipSlip vulnerability below
|
||||||
|
// nolint:gosec
|
||||||
|
fullPath := filepath.Join(dest, zf.Name)
|
||||||
|
|
||||||
|
// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
|
||||||
|
if filepath.IsAbs(zf.Name) ||
|
||||||
|
!strings.HasPrefix(fullPath, filepath.Clean(dest)+string(os.PathSeparator)) ||
|
||||||
|
strings.HasPrefix(zf.Name, ".."+string(os.PathSeparator)) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"archive member %q tries to write outside of plugin directory: %q, this can be a security risk",
|
||||||
|
zf.Name, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
dstPath := filepath.Clean(filepath.Join(dest, removeGitBuildFromName(zf.Name, pluginID)))
|
||||||
|
|
||||||
|
if zf.FileInfo().IsDir() {
|
||||||
|
// We can ignore gosec G304 here since it makes sense to give all users read access
|
||||||
|
// nolint:gosec
|
||||||
|
if err := os.MkdirAll(dstPath, 0755); err != nil {
|
||||||
|
if os.IsPermission(err) {
|
||||||
|
return fmt.Errorf(permissionsDeniedMessage, dstPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create needed directories to extract file
|
||||||
|
// We can ignore gosec G304 here since it makes sense to give all users read access
|
||||||
|
// nolint:gosec
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
|
||||||
|
return errutil.Wrap("failed to create directory to extract plugin files", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSymlink(zf) {
|
||||||
|
if !allowSymlinks {
|
||||||
|
i.log.Warnf("%v: plugin archive contains a symlink, which is not allowed. Skipping", zf.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := extractSymlink(zf, dstPath); err != nil {
|
||||||
|
i.log.Warn("failed to extract symlink", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := extractFile(zf, dstPath); err != nil {
|
||||||
|
return errutil.Wrap("failed to extract file", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSymlink(file *zip.File) bool {
|
||||||
|
return file.Mode()&os.ModeSymlink == os.ModeSymlink
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSymlink(file *zip.File, filePath string) error {
|
||||||
|
// symlink target is the contents of the file
|
||||||
|
src, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return errutil.Wrap("failed to extract file", err)
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if _, err := io.Copy(buf, src); err != nil {
|
||||||
|
return errutil.Wrap("failed to copy symlink contents", err)
|
||||||
|
}
|
||||||
|
if err := os.Symlink(strings.TrimSpace(buf.String()), filePath); err != nil {
|
||||||
|
return errutil.Wrapf(err, "failed to make symbolic link for %v", filePath)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFile(file *zip.File, filePath string) (err error) {
|
||||||
|
fileMode := file.Mode()
|
||||||
|
// This is entry point for backend plugins so we want to make them executable
|
||||||
|
if strings.HasSuffix(filePath, "_linux_amd64") || strings.HasSuffix(filePath, "_darwin_amd64") {
|
||||||
|
fileMode = os.FileMode(0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can ignore the gosec G304 warning on this one, since the variable part of the file path stems
|
||||||
|
// from command line flag "pluginsDir", and the only possible damage would be writing to the wrong directory.
|
||||||
|
// If the user shouldn't be writing to this directory, they shouldn't have the permission in the file system.
|
||||||
|
// nolint:gosec
|
||||||
|
dst, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsPermission(err) {
|
||||||
|
return fmt.Errorf(permissionsDeniedMessage, filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
unwrappedError := errors.Unwrap(err)
|
||||||
|
if unwrappedError != nil && strings.EqualFold(unwrappedError.Error(), "text file busy") {
|
||||||
|
return fmt.Errorf("file %q is in use - please stop Grafana, install the plugin and restart Grafana", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errutil.Wrap("failed to open file", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = dst.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
src, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return errutil.Wrap("failed to extract file", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = src.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = io.Copy(dst, src)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeGitBuildFromName(filename, pluginID string) string {
|
||||||
|
return reGitBuild.ReplaceAllString(filename, pluginID+"/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPluginDTO(pluginDir, pluginID string) (InstalledPlugin, error) {
|
||||||
|
distPluginDataPath := filepath.Join(pluginDir, pluginID, "dist", "plugin.json")
|
||||||
|
|
||||||
|
// It's safe to ignore gosec warning G304 since the file path suffix is hardcoded
|
||||||
|
// nolint:gosec
|
||||||
|
data, err := ioutil.ReadFile(distPluginDataPath)
|
||||||
|
if err != nil {
|
||||||
|
pluginDataPath := filepath.Join(pluginDir, pluginID, "plugin.json")
|
||||||
|
// It's safe to ignore gosec warning G304 since the file path suffix is hardcoded
|
||||||
|
// nolint:gosec
|
||||||
|
data, err = ioutil.ReadFile(pluginDataPath)
|
||||||
|
if err != nil {
|
||||||
|
return InstalledPlugin{}, errors.New("Could not find dist/plugin.json or plugin.json on " + pluginID + " in " + pluginDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := 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 InstalledPlugin{}, errors.New("could not find plugin " + pluginID + " in " + pluginDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
48
pkg/plugins/manager/installer/models.go
Normal file
48
pkg/plugins/manager/installer/models.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package installer
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Plugin struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Versions []Version `json:"versions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Version struct {
|
||||||
|
Commit string `json:"commit"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Arch map[string]ArchMeta `json:"arch"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArchMeta struct {
|
||||||
|
SHA256 string `json:"sha256"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginRepo struct {
|
||||||
|
Plugins []Plugin `json:"plugins"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
64
pkg/plugins/manager/logger.go
Normal file
64
pkg/plugins/manager/logger.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InfraLogWrapper struct {
|
||||||
|
l log.Logger
|
||||||
|
|
||||||
|
debugMode bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(name string, debugMode bool) (l *InfraLogWrapper) {
|
||||||
|
return &InfraLogWrapper{
|
||||||
|
debugMode: debugMode,
|
||||||
|
l: log.New(name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *InfraLogWrapper) Successf(format string, args ...interface{}) {
|
||||||
|
l.l.Info(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *InfraLogWrapper) Failuref(format string, args ...interface{}) {
|
||||||
|
l.l.Error(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *InfraLogWrapper) Info(args ...interface{}) {
|
||||||
|
l.l.Info(fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *InfraLogWrapper) Infof(format string, args ...interface{}) {
|
||||||
|
l.l.Info(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *InfraLogWrapper) Debug(args ...interface{}) {
|
||||||
|
if l.debugMode {
|
||||||
|
l.l.Debug(fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *InfraLogWrapper) Debugf(format string, args ...interface{}) {
|
||||||
|
if l.debugMode {
|
||||||
|
l.l.Debug(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *InfraLogWrapper) Warn(args ...interface{}) {
|
||||||
|
l.l.Warn(fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *InfraLogWrapper) Warnf(format string, args ...interface{}) {
|
||||||
|
l.l.Warn(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *InfraLogWrapper) Error(args ...interface{}) {
|
||||||
|
l.l.Error(fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *InfraLogWrapper) Errorf(format string, args ...interface{}) {
|
||||||
|
l.l.Error(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user