Files
grafana/pkg/api/datasources.go
Marcus Efraimsson 871ad73414 Backend plugins: Renderer v2 plugin (#23625)
grafana-plugin-model is legacy and is replaced by new backend 
plugins SDK and architecture. Renderer is not part of SDK and 
we want to keep it that way for now since it's highly unlikely there 
will be more than one kind of renderer plugin.
So this PR adds support for renderer plugin v2.
Also adds support sending a Device Scale Factor parameter to the 
plugin v2 remote rendering service and by that replaces #22474.
Adds support sending a Headers parameter to the plugin v2 and
remote rendering service which for now only include 
Accect-Language header (the user locale in browser when using 
Grafana), ref grafana/grafana-image-renderer#45.
Fixes health check json details response.
Adds image renderer plugin configuration settings in defaults.ini 
and sample.ini.

Co-Authored-By: Arve Knudsen <arve.knudsen@gmail.com>
2020-04-21 16:16:41 +02:00

413 lines
10 KiB
Go

package api
import (
"encoding/json"
"sort"
"github.com/grafana/grafana/pkg/api/dtos"
"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/util"
)
func GetDataSources(c *models.ReqContext) Response {
query := models.GetDataSourcesQuery{OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
return Error(500, "Failed to query datasources", err)
}
result := make(dtos.DataSourceList, 0)
for _, ds := range query.Result {
dsItem := dtos.DataSourceListItemDTO{
OrgId: ds.OrgId,
Id: ds.Id,
Name: ds.Name,
Url: ds.Url,
Type: ds.Type,
Access: ds.Access,
Password: ds.Password,
Database: ds.Database,
User: ds.User,
BasicAuth: ds.BasicAuth,
IsDefault: ds.IsDefault,
JsonData: ds.JsonData,
ReadOnly: ds.ReadOnly,
}
if plugin, exists := plugins.DataSources[ds.Type]; exists {
dsItem.TypeLogoUrl = plugin.Info.Logos.Small
} else {
dsItem.TypeLogoUrl = "public/img/icn-datasource.svg"
}
result = append(result, dsItem)
}
sort.Sort(result)
return JSON(200, &result)
}
func GetDataSourceById(c *models.ReqContext) Response {
query := models.GetDataSourceByIdQuery{
Id: c.ParamsInt64(":id"),
OrgId: c.OrgId,
}
if err := bus.Dispatch(&query); err != nil {
if err == models.ErrDataSourceNotFound {
return Error(404, "Data source not found", nil)
}
return Error(500, "Failed to query datasources", err)
}
ds := query.Result
dtos := convertModelToDtos(ds)
return JSON(200, &dtos)
}
func DeleteDataSourceById(c *models.ReqContext) Response {
id := c.ParamsInt64(":id")
if id <= 0 {
return Error(400, "Missing valid datasource id", nil)
}
ds, err := getRawDataSourceById(id, c.OrgId)
if err != nil {
return Error(400, "Failed to delete datasource", nil)
}
if ds.ReadOnly {
return Error(403, "Cannot delete read-only data source", nil)
}
cmd := &models.DeleteDataSourceByIdCommand{Id: id, OrgId: c.OrgId}
err = bus.Dispatch(cmd)
if err != nil {
return Error(500, "Failed to delete datasource", err)
}
return Success("Data source deleted")
}
func DeleteDataSourceByName(c *models.ReqContext) Response {
name := c.Params(":name")
if name == "" {
return Error(400, "Missing valid datasource name", nil)
}
getCmd := &models.GetDataSourceByNameQuery{Name: name, OrgId: c.OrgId}
if err := bus.Dispatch(getCmd); err != nil {
if err == models.ErrDataSourceNotFound {
return Error(404, "Data source not found", nil)
}
return Error(500, "Failed to delete datasource", err)
}
if getCmd.Result.ReadOnly {
return Error(403, "Cannot delete read-only data source", nil)
}
cmd := &models.DeleteDataSourceByNameCommand{Name: name, OrgId: c.OrgId}
err := bus.Dispatch(cmd)
if err != nil {
return Error(500, "Failed to delete datasource", err)
}
return Success("Data source deleted")
}
func AddDataSource(c *models.ReqContext, cmd models.AddDataSourceCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
if err == models.ErrDataSourceNameExists || err == models.ErrDataSourceUidExists {
return Error(409, err.Error(), err)
}
return Error(500, "Failed to add datasource", err)
}
ds := convertModelToDtos(cmd.Result)
return JSON(200, util.DynMap{
"message": "Datasource added",
"id": cmd.Result.Id,
"name": cmd.Result.Name,
"datasource": ds,
})
}
func UpdateDataSource(c *models.ReqContext, cmd models.UpdateDataSourceCommand) Response {
cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":id")
err := fillWithSecureJSONData(&cmd)
if err != nil {
return Error(500, "Failed to update datasource", err)
}
err = bus.Dispatch(&cmd)
if err != nil {
if err == models.ErrDataSourceUpdatingOldVersion {
return Error(500, "Failed to update datasource. Reload new version and try again", err)
}
return Error(500, "Failed to update datasource", err)
}
query := models.GetDataSourceByIdQuery{
Id: cmd.Id,
OrgId: c.OrgId,
}
if err := bus.Dispatch(&query); err != nil {
if err == models.ErrDataSourceNotFound {
return Error(404, "Data source not found", nil)
}
return Error(500, "Failed to query datasources", err)
}
dtos := convertModelToDtos(query.Result)
return JSON(200, util.DynMap{
"message": "Datasource updated",
"id": cmd.Id,
"name": cmd.Name,
"datasource": dtos,
})
}
func fillWithSecureJSONData(cmd *models.UpdateDataSourceCommand) error {
if len(cmd.SecureJsonData) == 0 {
return nil
}
ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId)
if err != nil {
return err
}
if ds.ReadOnly {
return models.ErrDatasourceIsReadOnly
}
secureJSONData := ds.SecureJsonData.Decrypt()
for k, v := range secureJSONData {
if _, ok := cmd.SecureJsonData[k]; !ok {
cmd.SecureJsonData[k] = v
}
}
return nil
}
func getRawDataSourceById(id int64, orgID int64) (*models.DataSource, error) {
query := models.GetDataSourceByIdQuery{
Id: id,
OrgId: orgID,
}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
return query.Result, nil
}
// Get /api/datasources/name/:name
func GetDataSourceByName(c *models.ReqContext) Response {
query := models.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
if err == models.ErrDataSourceNotFound {
return Error(404, "Data source not found", nil)
}
return Error(500, "Failed to query datasources", err)
}
dtos := convertModelToDtos(query.Result)
return JSON(200, &dtos)
}
// Get /api/datasources/id/:name
func GetDataSourceIdByName(c *models.ReqContext) Response {
query := models.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
if err == models.ErrDataSourceNotFound {
return Error(404, "Data source not found", nil)
}
return Error(500, "Failed to query datasources", err)
}
ds := query.Result
dtos := dtos.AnyId{
Id: ds.Id,
}
return JSON(200, &dtos)
}
// /api/datasources/:id/resources/*
func (hs *HTTPServer) CallDatasourceResource(c *models.ReqContext) {
datasourceID := c.ParamsInt64(":id")
ds, err := hs.DatasourceCache.GetDatasource(datasourceID, c.SignedInUser, c.SkipCache)
if err != nil {
if err == models.ErrDataSourceAccessDenied {
c.JsonApiErr(403, "Access denied to datasource", err)
return
}
c.JsonApiErr(500, "Unable to load datasource meta data", err)
return
}
// find plugin
plugin, ok := plugins.DataSources[ds.Type]
if !ok {
c.JsonApiErr(500, "Unable to find datasource plugin", err)
return
}
config := backendplugin.PluginConfig{
OrgID: c.OrgId,
PluginID: plugin.Id,
DataSourceConfig: &backendplugin.DataSourceConfig{
ID: ds.Id,
Name: ds.Name,
URL: ds.Url,
Database: ds.Database,
User: ds.User,
BasicAuthEnabled: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
JSONData: ds.JsonData,
DecryptedSecureJSONData: ds.DecryptedValues(),
Updated: ds.Updated,
},
}
hs.BackendPluginManager.CallResource(config, c, c.Params("*"))
}
func convertModelToDtos(ds *models.DataSource) dtos.DataSource {
dto := dtos.DataSource{
Id: ds.Id,
OrgId: ds.OrgId,
Name: ds.Name,
Url: ds.Url,
Type: ds.Type,
Access: ds.Access,
Password: ds.Password,
Database: ds.Database,
User: ds.User,
BasicAuth: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
BasicAuthPassword: ds.BasicAuthPassword,
WithCredentials: ds.WithCredentials,
IsDefault: ds.IsDefault,
JsonData: ds.JsonData,
SecureJsonFields: map[string]bool{},
Version: ds.Version,
ReadOnly: ds.ReadOnly,
}
for k, v := range ds.SecureJsonData {
if len(v) > 0 {
dto.SecureJsonFields[k] = true
}
}
return dto
}
// CheckDatasourceHealth sends a health check request to the plugin datasource
// /api/datasource/:id/health
func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) {
datasourceID := c.ParamsInt64("id")
ds, err := hs.DatasourceCache.GetDatasource(datasourceID, c.SignedInUser, c.SkipCache)
if err != nil {
if err == models.ErrDataSourceAccessDenied {
c.JsonApiErr(403, "Access denied to datasource", err)
return
}
c.JsonApiErr(500, "Unable to load datasource metadata", err)
return
}
plugin, ok := hs.PluginManager.GetDatasource(ds.Type)
if !ok {
c.JsonApiErr(500, "Unable to find datasource plugin", err)
return
}
config := &backendplugin.PluginConfig{
OrgID: c.OrgId,
PluginID: plugin.Id,
DataSourceConfig: &backendplugin.DataSourceConfig{
ID: ds.Id,
Name: ds.Name,
URL: ds.Url,
Database: ds.Database,
User: ds.User,
BasicAuthEnabled: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
JSONData: ds.JsonData,
DecryptedSecureJSONData: ds.DecryptedValues(),
Updated: ds.Updated,
},
}
resp, err := hs.BackendPluginManager.CheckHealth(c.Req.Context(), config)
if err != nil {
if err == backendplugin.ErrPluginNotRegistered {
c.JsonApiErr(404, "Plugin not found", err)
return
}
// Return status unknown instead?
if err == backendplugin.ErrDiagnosticsNotSupported {
c.JsonApiErr(404, "Health check not implemented", err)
return
}
// Return status unknown or error instead?
if err == backendplugin.ErrHealthCheckFailed {
c.JsonApiErr(500, "Plugin health check failed", err)
return
}
c.JsonApiErr(500, "Plugin healthcheck returned an unknown error", err)
return
}
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 {
c.JsonApiErr(500, "Failed to unmarshal detailed response from backend plugin", err)
return
}
payload["details"] = jsonDetails
}
if resp.Status != backendplugin.HealthStatusOk {
c.JSON(503, payload)
return
}
c.JSON(200, payload)
}