mirror of https://github.com/grafana/grafana
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 logpull/33368/head
parent
d0239ac958
commit
8e6205c107
@ -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() |
||||
} |
@ -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 |
||||
} |
@ -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"` |
||||
} |
@ -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...)) |
||||
} |
Loading…
Reference in new issue