grafana/pkg/api/plugins.go
Hugo Häggmark 3d41267fc4
Chore: Moves common and response into separate packages (#30298)
* Chore: moves common and response into separate packages

* Chore: moves common and response into separate packages

* Update pkg/api/utils/common.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Chore: changes after PR comments

* Chore: move wrap to routing package

* Chore: move functions in common to response package

* Chore: move functions in common to response package

* Chore: formats imports

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
2021-01-15 14:43:20 +01:00

393 lines
11 KiB
Go

package api
import (
"encoding/json"
"errors"
"net/http"
"sort"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/datasource/wrapper"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
)
// ErrPluginNotFound is returned when an requested plugin is not installed.
var ErrPluginNotFound error = errors.New("plugin not found, no installed plugin with that id")
func (hs *HTTPServer) getPluginContext(pluginID string, user *models.SignedInUser) (backend.PluginContext, error) {
pc := backend.PluginContext{}
plugin, exists := plugins.Plugins[pluginID]
if !exists {
return pc, ErrPluginNotFound
}
jsonData := json.RawMessage{}
decryptedSecureJSONData := map[string]string{}
var updated time.Time
ps, err := hs.getCachedPluginSettings(pluginID, user)
if err != nil {
// models.ErrPluginSettingNotFound is expected if there's no row found for plugin setting in database (if non-app plugin).
// If it's not this expected error something is wrong with cache or database and we return the error to the client.
if !errors.Is(err, models.ErrPluginSettingNotFound) {
return pc, errutil.Wrap("Failed to get plugin settings", err)
}
} else {
jsonData, err = json.Marshal(ps.JsonData)
if err != nil {
return pc, errutil.Wrap("Failed to unmarshal plugin json data", err)
}
decryptedSecureJSONData = ps.DecryptedValues()
updated = ps.Updated
}
return backend.PluginContext{
OrgID: user.OrgId,
PluginID: plugin.Id,
User: wrapper.BackendUserFromSignedInUser(user),
AppInstanceSettings: &backend.AppInstanceSettings{
JSONData: jsonData,
DecryptedSecureJSONData: decryptedSecureJSONData,
Updated: updated,
},
}, nil
}
func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
typeFilter := c.Query("type")
enabledFilter := c.Query("enabled")
embeddedFilter := c.Query("embedded")
coreFilter := c.Query("core")
// For users with viewer role we only return core plugins
if !c.HasRole(models.ROLE_ADMIN) {
coreFilter = "1"
}
pluginSettingsMap, err := plugins.GetPluginSettings(c.OrgId)
if err != nil {
return response.Error(500, "Failed to get list of plugins", err)
}
result := make(dtos.PluginList, 0)
for _, pluginDef := range plugins.Plugins {
// filter out app sub plugins
if embeddedFilter == "0" && pluginDef.IncludedInAppId != "" {
continue
}
// filter out core plugins
if (coreFilter == "0" && pluginDef.IsCorePlugin) || (coreFilter == "1" && !pluginDef.IsCorePlugin) {
continue
}
// filter on type
if typeFilter != "" && typeFilter != pluginDef.Type {
continue
}
if pluginDef.State == plugins.PluginStateAlpha && !hs.Cfg.PluginsEnableAlpha {
continue
}
listItem := dtos.PluginListItem{
Id: pluginDef.Id,
Name: pluginDef.Name,
Type: pluginDef.Type,
Category: pluginDef.Category,
Info: &pluginDef.Info,
LatestVersion: pluginDef.GrafanaNetVersion,
HasUpdate: pluginDef.GrafanaNetHasUpdate,
DefaultNavUrl: pluginDef.DefaultNavUrl,
State: pluginDef.State,
Signature: pluginDef.Signature,
SignatureType: pluginDef.SignatureType,
SignatureOrg: pluginDef.SignatureOrg,
}
if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists {
listItem.Enabled = pluginSetting.Enabled
listItem.Pinned = pluginSetting.Pinned
}
if listItem.DefaultNavUrl == "" || !listItem.Enabled {
listItem.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + listItem.Id + "/"
}
// filter out disabled plugins
if enabledFilter == "1" && !listItem.Enabled {
continue
}
// filter out built in data sources
if ds, exists := plugins.DataSources[pluginDef.Id]; exists {
if ds.BuiltIn {
continue
}
}
result = append(result, listItem)
}
sort.Sort(result)
return response.JSON(200, result)
}
func GetPluginSettingByID(c *models.ReqContext) response.Response {
pluginID := c.Params(":pluginId")
def, exists := plugins.Plugins[pluginID]
if !exists {
return response.Error(404, "Plugin not found, no installed plugin with that id", nil)
}
dto := &dtos.PluginSetting{
Type: def.Type,
Id: def.Id,
Name: def.Name,
Info: &def.Info,
Dependencies: &def.Dependencies,
Includes: def.Includes,
BaseUrl: def.BaseUrl,
Module: def.Module,
DefaultNavUrl: def.DefaultNavUrl,
LatestVersion: def.GrafanaNetVersion,
HasUpdate: def.GrafanaNetHasUpdate,
State: def.State,
Signature: def.Signature,
SignatureType: def.SignatureType,
SignatureOrg: def.SignatureOrg,
}
query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
if !errors.Is(err, models.ErrPluginSettingNotFound) {
return response.Error(500, "Failed to get login settings", nil)
}
} else {
dto.Enabled = query.Result.Enabled
dto.Pinned = query.Result.Pinned
dto.JsonData = query.Result.JsonData
}
return response.JSON(200, dto)
}
func UpdatePluginSetting(c *models.ReqContext, cmd models.UpdatePluginSettingCmd) response.Response {
pluginID := c.Params(":pluginId")
cmd.OrgId = c.OrgId
cmd.PluginId = pluginID
if _, ok := plugins.Apps[cmd.PluginId]; !ok {
return response.Error(404, "Plugin not installed.", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
return response.Error(500, "Failed to update plugin setting", err)
}
return response.Success("Plugin settings updated")
}
func GetPluginDashboards(c *models.ReqContext) response.Response {
pluginID := c.Params(":pluginId")
list, err := plugins.GetPluginDashboards(c.OrgId, pluginID)
if err != nil {
var notFound plugins.PluginNotFoundError
if errors.As(err, &notFound) {
return response.Error(404, notFound.Error(), nil)
}
return response.Error(500, "Failed to get plugin dashboards", err)
}
return response.JSON(200, list)
}
func GetPluginMarkdown(c *models.ReqContext) response.Response {
pluginID := c.Params(":pluginId")
name := c.Params(":name")
content, err := plugins.GetPluginMarkdown(pluginID, name)
if err != nil {
var notFound plugins.PluginNotFoundError
if errors.As(err, &notFound) {
return response.Error(404, notFound.Error(), nil)
}
return response.Error(500, "Could not get markdown file", err)
}
// fallback try readme
if len(content) == 0 {
content, err = plugins.GetPluginMarkdown(pluginID, "readme")
if err != nil {
return response.Error(501, "Could not get markdown file", err)
}
}
resp := response.Respond(200, content)
resp.Header("Content-Type", "text/plain; charset=utf-8")
return resp
}
func ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDashboardCommand) response.Response {
if apiCmd.PluginId == "" && apiCmd.Dashboard == nil {
return response.Error(422, "Dashboard must be set", nil)
}
cmd := plugins.ImportDashboardCommand{
OrgId: c.OrgId,
User: c.SignedInUser,
PluginId: apiCmd.PluginId,
Path: apiCmd.Path,
Inputs: apiCmd.Inputs,
Overwrite: apiCmd.Overwrite,
FolderId: apiCmd.FolderId,
Dashboard: apiCmd.Dashboard,
}
if err := bus.Dispatch(&cmd); err != nil {
return dashboardSaveErrorToApiResponse(err)
}
return response.JSON(200, cmd.Result)
}
// CollectPluginMetrics collect metrics from a plugin.
//
// /api/plugins/:pluginId/metrics
func (hs *HTTPServer) CollectPluginMetrics(c *models.ReqContext) response.Response {
pluginID := c.Params("pluginId")
plugin, exists := plugins.Plugins[pluginID]
if !exists {
return response.Error(404, "Plugin not found", nil)
}
resp, err := hs.BackendPluginManager.CollectMetrics(c.Req.Context(), plugin.Id)
if err != nil {
return translatePluginRequestErrorToAPIError(err)
}
headers := make(http.Header)
headers.Set("Content-Type", "text/plain")
return response.CreateNormalResponse(headers, resp.PrometheusMetrics, http.StatusOK)
}
// CheckHealth returns the health of a plugin.
// /api/plugins/:pluginId/health
func (hs *HTTPServer) CheckHealth(c *models.ReqContext) response.Response {
pluginID := c.Params("pluginId")
pCtx, err := hs.getPluginContext(pluginID, c.SignedInUser)
if err != nil {
if errors.Is(err, ErrPluginNotFound) {
return response.Error(404, "Plugin not found", nil)
}
return response.Error(500, "Failed to get plugin settings", err)
}
resp, err := hs.BackendPluginManager.CheckHealth(c.Req.Context(), pCtx)
if err != nil {
return translatePluginRequestErrorToAPIError(err)
}
payload := map[string]interface{}{
"status": resp.Status.String(),
"message": resp.Message,
}
// Unmarshal JSONDetails if it's not empty.
if len(resp.JSONDetails) > 0 {
var jsonDetails map[string]interface{}
err = json.Unmarshal(resp.JSONDetails, &jsonDetails)
if err != nil {
return response.Error(500, "Failed to unmarshal detailed response from backend plugin", err)
}
payload["details"] = jsonDetails
}
if resp.Status != backend.HealthStatusOk {
return response.JSON(503, payload)
}
return response.JSON(200, payload)
}
// CallResource passes a resource call from a plugin to the backend plugin.
//
// /api/plugins/:pluginId/resources/*
func (hs *HTTPServer) CallResource(c *models.ReqContext) {
pluginID := c.Params("pluginId")
pCtx, err := hs.getPluginContext(pluginID, c.SignedInUser)
if err != nil {
if errors.Is(err, ErrPluginNotFound) {
c.JsonApiErr(404, "Plugin not found", nil)
return
}
c.JsonApiErr(500, "Failed to get plugin settings", err)
return
}
hs.BackendPluginManager.CallResource(pCtx, c, c.Params("*"))
}
func (hs *HTTPServer) getCachedPluginSettings(pluginID string, user *models.SignedInUser) (*models.PluginSetting, error) {
cacheKey := "plugin-setting-" + pluginID
if cached, found := hs.CacheService.Get(cacheKey); found {
ps := cached.(*models.PluginSetting)
if ps.OrgId == user.OrgId {
return ps, nil
}
}
query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: user.OrgId}
if err := hs.Bus.Dispatch(&query); err != nil {
return nil, err
}
hs.CacheService.Set(cacheKey, query.Result, time.Second*5)
return query.Result, nil
}
func (hs *HTTPServer) GetPluginErrorsList(c *models.ReqContext) response.Response {
return response.JSON(200, plugins.ScanningErrors())
}
func translatePluginRequestErrorToAPIError(err error) response.Response {
if errors.Is(err, backendplugin.ErrPluginNotRegistered) {
return response.Error(404, "Plugin not found", err)
}
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
return response.Error(404, "Not found", err)
}
if errors.Is(err, backendplugin.ErrHealthCheckFailed) {
return response.Error(500, "Plugin health check failed", err)
}
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
return response.Error(503, "Plugin unavailable", err)
}
return response.Error(500, "Plugin request failed", err)
}