mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 18:13:32 -06:00
* 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
217 lines
5.8 KiB
Go
217 lines
5.8 KiB
Go
package services
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"runtime"
|
|
|
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
|
"github.com/grafana/grafana/pkg/util/errutil"
|
|
)
|
|
|
|
type GrafanaComClient struct {
|
|
retryCount int
|
|
}
|
|
|
|
func (client *GrafanaComClient) GetPlugin(pluginId, repoUrl string) (models.Plugin, error) {
|
|
logger.Debugf("getting plugin metadata from: %v pluginId: %v \n", repoUrl, pluginId)
|
|
body, err := sendRequestGetBytes(HttpClient, repoUrl, "repo", pluginId)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNotFoundError) {
|
|
return models.Plugin{}, errutil.Wrap(
|
|
fmt.Sprintf("Failed to find requested plugin, check if the plugin_id (%s) is correct", pluginId), err)
|
|
}
|
|
return models.Plugin{}, errutil.Wrap("Failed to send request", err)
|
|
}
|
|
|
|
var data models.Plugin
|
|
err = json.Unmarshal(body, &data)
|
|
if err != nil {
|
|
logger.Info("Failed to unmarshal plugin repo response error:", err)
|
|
return models.Plugin{}, err
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (client *GrafanaComClient) DownloadFile(pluginName 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)
|
|
}
|
|
_, err = io.Copy(tmpFile, f)
|
|
if err != nil {
|
|
return errutil.Wrap("Failed to copy plugin archive", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
client.retryCount = 0
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
client.retryCount++
|
|
if client.retryCount < 3 {
|
|
logger.Info("Failed downloading. Will retry once.")
|
|
err = tmpFile.Truncate(0)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, err = tmpFile.Seek(0, 0)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = client.DownloadFile(pluginName, tmpFile, url, checksum)
|
|
} else {
|
|
client.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 := sendRequest(HttpClientNoTimeout, url)
|
|
if err != nil {
|
|
return errutil.Wrap("Failed to send request", err)
|
|
}
|
|
defer func() {
|
|
if err := bodyReader.Close(); err != nil {
|
|
logger.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 (client *GrafanaComClient) ListAllPlugins(repoUrl string) (models.PluginRepo, error) {
|
|
body, err := sendRequestGetBytes(HttpClient, repoUrl, "repo")
|
|
|
|
if err != nil {
|
|
logger.Info("Failed to send request", "error", err)
|
|
return models.PluginRepo{}, errutil.Wrap("Failed to send request", err)
|
|
}
|
|
|
|
var data models.PluginRepo
|
|
err = json.Unmarshal(body, &data)
|
|
if err != nil {
|
|
logger.Info("Failed to unmarshal plugin repo response error:", err)
|
|
return models.PluginRepo{}, err
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func sendRequestGetBytes(client http.Client, repoUrl string, subPaths ...string) ([]byte, error) {
|
|
bodyReader, err := sendRequest(client, repoUrl, subPaths...)
|
|
if err != nil {
|
|
return []byte{}, err
|
|
}
|
|
defer func() {
|
|
if err := bodyReader.Close(); err != nil {
|
|
logger.Warn("Failed to close stream", "err", err)
|
|
}
|
|
}()
|
|
return ioutil.ReadAll(bodyReader)
|
|
}
|
|
|
|
func sendRequest(client http.Client, repoUrl string, subPaths ...string) (io.ReadCloser, error) {
|
|
req, err := createRequest(repoUrl, subPaths...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return handleResponse(res)
|
|
}
|
|
|
|
func createRequest(repoUrl string, subPaths ...string) (*http.Request, error) {
|
|
u, err := url.Parse(repoUrl)
|
|
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", GrafanaVersion)
|
|
req.Header.Set("grafana-os", runtime.GOOS)
|
|
req.Header.Set("grafana-arch", runtime.GOARCH)
|
|
req.Header.Set("User-Agent", "grafana "+GrafanaVersion)
|
|
|
|
return req, err
|
|
}
|
|
|
|
func 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 {
|
|
logger.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
|
|
}
|