mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 02:02:31 +08:00
Add support for sending health check to datasource plugins. (#22771)
closes #21519 ref grafana/grafana-plugin-sdk-go#93
This commit is contained in:
2
go.mod
2
go.mod
@ -32,7 +32,7 @@ require (
|
|||||||
github.com/gorilla/websocket v1.4.1
|
github.com/gorilla/websocket v1.4.1
|
||||||
github.com/gosimple/slug v1.4.2
|
github.com/gosimple/slug v1.4.2
|
||||||
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4
|
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.26.0
|
github.com/grafana/grafana-plugin-sdk-go v0.28.0
|
||||||
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd
|
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd
|
||||||
github.com/hashicorp/go-plugin v1.0.1
|
github.com/hashicorp/go-plugin v1.0.1
|
||||||
github.com/hashicorp/go-version v1.1.0
|
github.com/hashicorp/go-version v1.1.0
|
||||||
|
10
go.sum
10
go.sum
@ -133,14 +133,8 @@ github.com/gosimple/slug v1.4.2 h1:jDmprx3q/9Lfk4FkGZtvzDQ9Cj9eAmsjzeQGp24PeiQ=
|
|||||||
github.com/gosimple/slug v1.4.2/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0=
|
github.com/gosimple/slug v1.4.2/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0=
|
||||||
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4 h1:SPdxCL9BChFTlyi0Khv64vdCW4TMna8+sxL7+Chx+Ag=
|
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4 h1:SPdxCL9BChFTlyi0Khv64vdCW4TMna8+sxL7+Chx+Ag=
|
||||||
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4/go.mod h1:nc0XxBzjeGcrMltCDw269LoWF9S8ibhgxolCdA1R8To=
|
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4/go.mod h1:nc0XxBzjeGcrMltCDw269LoWF9S8ibhgxolCdA1R8To=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.21.0 h1:5en5MdVFgeD9tuHDuJgwHYdIVjPs0PN0a7ZQ2bZNxNk=
|
github.com/grafana/grafana-plugin-sdk-go v0.28.0 h1:vRyaOOzpvqKhh4619Woepqe7wen4juQyKFxPFPFz1wE=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.21.0/go.mod h1:G6Ov9M+FDOZXNw8eKXINO6XzqdUvTs7huwyQp5jLTBQ=
|
github.com/grafana/grafana-plugin-sdk-go v0.28.0/go.mod h1:G6Ov9M+FDOZXNw8eKXINO6XzqdUvTs7huwyQp5jLTBQ=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.22.1-0.20200310164332-6b4c0d952d70 h1:VQFBaWHlxwjb4VB5HuXtuucMzXJ7xZGGASzbqA3VtVo=
|
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.22.1-0.20200310164332-6b4c0d952d70/go.mod h1:G6Ov9M+FDOZXNw8eKXINO6XzqdUvTs7huwyQp5jLTBQ=
|
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.24.0 h1:sgd9rAQMmB0rAIMd4JVMFM0Gc+CTHoDwN5oxkPjVrGw=
|
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.24.0/go.mod h1:G6Ov9M+FDOZXNw8eKXINO6XzqdUvTs7huwyQp5jLTBQ=
|
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.26.0 h1:zDOZMGgGOrFF5m7+iqcQSQA/AJiG9xplNibL8SbLmn4=
|
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.26.0/go.mod h1:G6Ov9M+FDOZXNw8eKXINO6XzqdUvTs7huwyQp5jLTBQ=
|
|
||||||
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd h1:rNuUHR+CvK1IS89MMtcF0EpcVMZtjKfPRp4MEmt/aTs=
|
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd h1:rNuUHR+CvK1IS89MMtcF0EpcVMZtjKfPRp4MEmt/aTs=
|
||||||
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
|
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
|
||||||
github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE=
|
github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE=
|
||||||
|
@ -265,6 +265,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
|
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
|
||||||
apiRoute.Any("/datasources/:id/resources", hs.CallDatasourceResource)
|
apiRoute.Any("/datasources/:id/resources", hs.CallDatasourceResource)
|
||||||
apiRoute.Any("/datasources/:id/resources/*", hs.CallDatasourceResource)
|
apiRoute.Any("/datasources/:id/resources/*", hs.CallDatasourceResource)
|
||||||
|
apiRoute.Any("/datasources/:id/health", hs.CheckDatasourceHealth)
|
||||||
|
|
||||||
// Folders
|
// Folders
|
||||||
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
||||||
|
@ -323,3 +323,78 @@ func convertModelToDtos(ds *models.DataSource) dtos.DataSource {
|
|||||||
|
|
||||||
return dto
|
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,
|
||||||
|
"jsonDetails": resp.JSONDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Status != backendplugin.HealthStatusOk {
|
||||||
|
c.JSON(503, payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, payload)
|
||||||
|
}
|
||||||
|
@ -73,6 +73,7 @@ type HTTPServer struct {
|
|||||||
Login *login.LoginService `inject:""`
|
Login *login.LoginService `inject:""`
|
||||||
License models.Licensing `inject:""`
|
License models.Licensing `inject:""`
|
||||||
BackendPluginManager backendplugin.Manager `inject:""`
|
BackendPluginManager backendplugin.Manager `inject:""`
|
||||||
|
PluginManager *plugins.PluginManager `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) Init() error {
|
func (hs *HTTPServer) Init() error {
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
@ -14,6 +16,41 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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) getPluginConfig(pluginID string, user *models.SignedInUser) (backendplugin.PluginConfig, error) {
|
||||||
|
pluginConfig := backendplugin.PluginConfig{}
|
||||||
|
plugin, exists := plugins.Plugins[pluginID]
|
||||||
|
if !exists {
|
||||||
|
return pluginConfig, ErrPluginNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonData *simplejson.Json
|
||||||
|
var decryptedSecureJSONData map[string]string
|
||||||
|
var updated time.Time
|
||||||
|
|
||||||
|
ps, err := hs.getCachedPluginSettings(pluginID, user)
|
||||||
|
if err != nil {
|
||||||
|
if err != models.ErrPluginSettingNotFound {
|
||||||
|
return pluginConfig, errutil.Wrap("Failed to get plugin settings", err)
|
||||||
|
}
|
||||||
|
jsonData = simplejson.New()
|
||||||
|
decryptedSecureJSONData = make(map[string]string)
|
||||||
|
} else {
|
||||||
|
decryptedSecureJSONData = ps.DecryptedValues()
|
||||||
|
updated = ps.Updated
|
||||||
|
}
|
||||||
|
|
||||||
|
return backendplugin.PluginConfig{
|
||||||
|
OrgID: user.OrgId,
|
||||||
|
PluginID: plugin.Id,
|
||||||
|
JSONData: jsonData,
|
||||||
|
DecryptedSecureJSONData: decryptedSecureJSONData,
|
||||||
|
Updated: updated,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) GetPluginList(c *models.ReqContext) Response {
|
func (hs *HTTPServer) GetPluginList(c *models.ReqContext) Response {
|
||||||
typeFilter := c.Query("type")
|
typeFilter := c.Query("type")
|
||||||
enabledFilter := c.Query("enabled")
|
enabledFilter := c.Query("enabled")
|
||||||
@ -209,7 +246,17 @@ func ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDashboardCommand) R
|
|||||||
// /api/plugins/:pluginId/health
|
// /api/plugins/:pluginId/health
|
||||||
func (hs *HTTPServer) CheckHealth(c *models.ReqContext) Response {
|
func (hs *HTTPServer) CheckHealth(c *models.ReqContext) Response {
|
||||||
pluginID := c.Params("pluginId")
|
pluginID := c.Params("pluginId")
|
||||||
resp, err := hs.BackendPluginManager.CheckHealth(c.Req.Context(), pluginID)
|
|
||||||
|
config, err := hs.getPluginConfig(pluginID, c.SignedInUser)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrPluginNotFound {
|
||||||
|
return Error(404, "Plugin not found, no installed plugin with that id", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Error(500, "Failed to get plugin settings", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := hs.BackendPluginManager.CheckHealth(c.Req.Context(), &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == backendplugin.ErrPluginNotRegistered {
|
if err == backendplugin.ErrPluginNotRegistered {
|
||||||
return Error(404, "Plugin not found", err)
|
return Error(404, "Plugin not found", err)
|
||||||
@ -224,6 +271,8 @@ func (hs *HTTPServer) CheckHealth(c *models.ReqContext) Response {
|
|||||||
if err == backendplugin.ErrHealthCheckFailed {
|
if err == backendplugin.ErrHealthCheckFailed {
|
||||||
return Error(500, "Plugin health check failed", err)
|
return Error(500, "Plugin health check failed", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Error(500, "Plugin healthcheck returned an unknown error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
@ -239,39 +288,23 @@ func (hs *HTTPServer) CheckHealth(c *models.ReqContext) Response {
|
|||||||
return JSON(200, payload)
|
return JSON(200, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CallResource passes a resource call from a plugin to the backend plugin.
|
||||||
|
//
|
||||||
// /api/plugins/:pluginId/resources/*
|
// /api/plugins/:pluginId/resources/*
|
||||||
func (hs *HTTPServer) CallResource(c *models.ReqContext) {
|
func (hs *HTTPServer) CallResource(c *models.ReqContext) {
|
||||||
pluginID := c.Params("pluginId")
|
pluginID := c.Params("pluginId")
|
||||||
plugin, exists := plugins.Plugins[pluginID]
|
|
||||||
if !exists {
|
config, err := hs.getPluginConfig(pluginID, c.SignedInUser)
|
||||||
c.JsonApiErr(404, "Plugin not found, no installed plugin with that id", nil)
|
if err != nil {
|
||||||
|
if err == ErrPluginNotFound {
|
||||||
|
c.JsonApiErr(404, "Plugin not found, no installed plugin with that id", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JsonApiErr(500, "Failed to get plugin settings", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonData *simplejson.Json
|
|
||||||
var decryptedSecureJSONData map[string]string
|
|
||||||
var updated time.Time
|
|
||||||
|
|
||||||
ps, err := hs.getCachedPluginSettings(pluginID, c.SignedInUser)
|
|
||||||
if err != nil {
|
|
||||||
if err != models.ErrPluginSettingNotFound {
|
|
||||||
c.JsonApiErr(500, "Failed to get plugin settings", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonData = simplejson.New()
|
|
||||||
decryptedSecureJSONData = make(map[string]string)
|
|
||||||
} else {
|
|
||||||
decryptedSecureJSONData = ps.DecryptedValues()
|
|
||||||
updated = ps.Updated
|
|
||||||
}
|
|
||||||
|
|
||||||
config := backendplugin.PluginConfig{
|
|
||||||
OrgID: c.OrgId,
|
|
||||||
PluginID: plugin.Id,
|
|
||||||
JSONData: jsonData,
|
|
||||||
DecryptedSecureJSONData: decryptedSecureJSONData,
|
|
||||||
Updated: updated,
|
|
||||||
}
|
|
||||||
hs.BackendPluginManager.CallResource(config, c, c.Params("*"))
|
hs.BackendPluginManager.CallResource(config, c, c.Params("*"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,14 +185,27 @@ func (p *BackendPlugin) CollectMetrics(ctx context.Context, ch chan<- prometheus
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BackendPlugin) checkHealth(ctx context.Context) (*pluginv2.CheckHealthResponse, error) {
|
func (p *BackendPlugin) checkHealth(ctx context.Context, config *PluginConfig) (*pluginv2.CheckHealthResponse, error) {
|
||||||
if p.diagnostics == nil || p.client == nil || p.client.Exited() {
|
if p.diagnostics == nil || p.client == nil || p.client.Exited() {
|
||||||
return &pluginv2.CheckHealthResponse{
|
return &pluginv2.CheckHealthResponse{
|
||||||
Status: pluginv2.CheckHealthResponse_UNKNOWN,
|
Status: pluginv2.CheckHealthResponse_UNKNOWN,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := p.diagnostics.CheckHealth(ctx, &pluginv2.CheckHealthRequest{})
|
jsonDataBytes, err := config.JSONData.ToDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pconfig := &pluginv2.PluginConfig{
|
||||||
|
OrgId: config.OrgID,
|
||||||
|
PluginId: config.PluginID,
|
||||||
|
JsonData: jsonDataBytes,
|
||||||
|
DecryptedSecureJsonData: config.DecryptedSecureJSONData,
|
||||||
|
LastUpdatedMS: config.Updated.UnixNano() / int64(time.Millisecond),
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := p.diagnostics.CheckHealth(ctx, &pluginv2.CheckHealthRequest{Config: pconfig})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if st, ok := status.FromError(err); ok {
|
if st, ok := status.FromError(err); ok {
|
||||||
if st.Code() == codes.Unimplemented {
|
if st.Code() == codes.Unimplemented {
|
||||||
|
@ -101,7 +101,7 @@ func NewRendererPluginDescriptor(pluginID, executablePath string, startFns Plugi
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DiagnosticsPlugin interface {
|
type DiagnosticsPlugin interface {
|
||||||
plugin.DiagnosticsServer
|
plugin.DiagnosticsClient
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourcePlugin interface {
|
type ResourcePlugin interface {
|
||||||
|
@ -43,7 +43,7 @@ type Manager interface {
|
|||||||
// StartPlugin starts a non-managed backend plugin
|
// StartPlugin starts a non-managed backend plugin
|
||||||
StartPlugin(ctx context.Context, pluginID string) error
|
StartPlugin(ctx context.Context, pluginID string) error
|
||||||
// CheckHealth checks the health of a registered backend plugin.
|
// CheckHealth checks the health of a registered backend plugin.
|
||||||
CheckHealth(ctx context.Context, pluginID string) (*CheckHealthResult, error)
|
CheckHealth(ctx context.Context, pluginConfig *PluginConfig) (*CheckHealthResult, error)
|
||||||
// CallResource calls a plugin resource.
|
// CallResource calls a plugin resource.
|
||||||
CallResource(pluginConfig PluginConfig, ctx *models.ReqContext, path string)
|
CallResource(pluginConfig PluginConfig, ctx *models.ReqContext, path string)
|
||||||
}
|
}
|
||||||
@ -151,9 +151,9 @@ func (m *manager) stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckHealth checks the health of a registered backend plugin.
|
// CheckHealth checks the health of a registered backend plugin.
|
||||||
func (m *manager) CheckHealth(ctx context.Context, pluginID string) (*CheckHealthResult, error) {
|
func (m *manager) CheckHealth(ctx context.Context, pluginConfig *PluginConfig) (*CheckHealthResult, error) {
|
||||||
m.pluginsMu.RLock()
|
m.pluginsMu.RLock()
|
||||||
p, registered := m.plugins[pluginID]
|
p, registered := m.plugins[pluginConfig.PluginID]
|
||||||
m.pluginsMu.RUnlock()
|
m.pluginsMu.RUnlock()
|
||||||
|
|
||||||
if !registered {
|
if !registered {
|
||||||
@ -164,7 +164,7 @@ func (m *manager) CheckHealth(ctx context.Context, pluginID string) (*CheckHealt
|
|||||||
return nil, ErrDiagnosticsNotSupported
|
return nil, ErrDiagnosticsNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := p.checkHealth(ctx)
|
res, err := p.checkHealth(ctx, pluginConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.logger.Error("Failed to check plugin health", "error", err)
|
p.logger.Error("Failed to check plugin health", "error", err)
|
||||||
return nil, ErrHealthCheckFailed
|
return nil, ErrHealthCheckFailed
|
||||||
|
@ -188,6 +188,15 @@ func (pm *PluginManager) scan(pluginDir string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDatasource returns a datasource based on passed pluginID if it exists
|
||||||
|
//
|
||||||
|
// This function fetches the datasource from the global variable DataSources in this package.
|
||||||
|
// Rather then refactor all dependencies on the global variable we can use this as an transition.
|
||||||
|
func (pm *PluginManager) GetDatasource(pluginID string) (*DataSourcePlugin, bool) {
|
||||||
|
ds, exist := DataSources[pluginID]
|
||||||
|
return ds, exist
|
||||||
|
}
|
||||||
|
|
||||||
func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err error) error {
|
func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err error) error {
|
||||||
// We scan all the subfolders for plugin.json (with some exceptions) so that we also load embedded plugins, for
|
// We scan all the subfolders for plugin.json (with some exceptions) so that we also load embedded plugins, for
|
||||||
// example https://github.com/raintank/worldping-app/tree/master/dist/grafana-worldmap-panel worldmap panel plugin
|
// example https://github.com/raintank/worldping-app/tree/master/dist/grafana-worldmap-panel worldmap panel plugin
|
||||||
|
89
vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go
generated
vendored
Normal file
89
vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go
generated
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE.md file.
|
||||||
|
|
||||||
|
// Package cmpopts provides common options for the cmp package.
|
||||||
|
package cmpopts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func equateAlways(_, _ interface{}) bool { return true }
|
||||||
|
|
||||||
|
// EquateEmpty returns a Comparer option that determines all maps and slices
|
||||||
|
// with a length of zero to be equal, regardless of whether they are nil.
|
||||||
|
//
|
||||||
|
// EquateEmpty can be used in conjunction with SortSlices and SortMaps.
|
||||||
|
func EquateEmpty() cmp.Option {
|
||||||
|
return cmp.FilterValues(isEmpty, cmp.Comparer(equateAlways))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmpty(x, y interface{}) bool {
|
||||||
|
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||||
|
return (x != nil && y != nil && vx.Type() == vy.Type()) &&
|
||||||
|
(vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) &&
|
||||||
|
(vx.Len() == 0 && vy.Len() == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EquateApprox returns a Comparer option that determines float32 or float64
|
||||||
|
// values to be equal if they are within a relative fraction or absolute margin.
|
||||||
|
// This option is not used when either x or y is NaN or infinite.
|
||||||
|
//
|
||||||
|
// The fraction determines that the difference of two values must be within the
|
||||||
|
// smaller fraction of the two values, while the margin determines that the two
|
||||||
|
// values must be within some absolute margin.
|
||||||
|
// To express only a fraction or only a margin, use 0 for the other parameter.
|
||||||
|
// The fraction and margin must be non-negative.
|
||||||
|
//
|
||||||
|
// The mathematical expression used is equivalent to:
|
||||||
|
// |x-y| ≤ max(fraction*min(|x|, |y|), margin)
|
||||||
|
//
|
||||||
|
// EquateApprox can be used in conjunction with EquateNaNs.
|
||||||
|
func EquateApprox(fraction, margin float64) cmp.Option {
|
||||||
|
if margin < 0 || fraction < 0 || math.IsNaN(margin) || math.IsNaN(fraction) {
|
||||||
|
panic("margin or fraction must be a non-negative number")
|
||||||
|
}
|
||||||
|
a := approximator{fraction, margin}
|
||||||
|
return cmp.Options{
|
||||||
|
cmp.FilterValues(areRealF64s, cmp.Comparer(a.compareF64)),
|
||||||
|
cmp.FilterValues(areRealF32s, cmp.Comparer(a.compareF32)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type approximator struct{ frac, marg float64 }
|
||||||
|
|
||||||
|
func areRealF64s(x, y float64) bool {
|
||||||
|
return !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0)
|
||||||
|
}
|
||||||
|
func areRealF32s(x, y float32) bool {
|
||||||
|
return areRealF64s(float64(x), float64(y))
|
||||||
|
}
|
||||||
|
func (a approximator) compareF64(x, y float64) bool {
|
||||||
|
relMarg := a.frac * math.Min(math.Abs(x), math.Abs(y))
|
||||||
|
return math.Abs(x-y) <= math.Max(a.marg, relMarg)
|
||||||
|
}
|
||||||
|
func (a approximator) compareF32(x, y float32) bool {
|
||||||
|
return a.compareF64(float64(x), float64(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EquateNaNs returns a Comparer option that determines float32 and float64
|
||||||
|
// NaN values to be equal.
|
||||||
|
//
|
||||||
|
// EquateNaNs can be used in conjunction with EquateApprox.
|
||||||
|
func EquateNaNs() cmp.Option {
|
||||||
|
return cmp.Options{
|
||||||
|
cmp.FilterValues(areNaNsF64s, cmp.Comparer(equateAlways)),
|
||||||
|
cmp.FilterValues(areNaNsF32s, cmp.Comparer(equateAlways)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func areNaNsF64s(x, y float64) bool {
|
||||||
|
return math.IsNaN(x) && math.IsNaN(y)
|
||||||
|
}
|
||||||
|
func areNaNsF32s(x, y float32) bool {
|
||||||
|
return areNaNsF64s(float64(x), float64(y))
|
||||||
|
}
|
207
vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go
generated
vendored
Normal file
207
vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go
generated
vendored
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE.md file.
|
||||||
|
|
||||||
|
package cmpopts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/internal/function"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IgnoreFields returns an Option that ignores exported fields of the
|
||||||
|
// given names on a single struct type.
|
||||||
|
// The struct type is specified by passing in a value of that type.
|
||||||
|
//
|
||||||
|
// The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a
|
||||||
|
// specific sub-field that is embedded or nested within the parent struct.
|
||||||
|
//
|
||||||
|
// This does not handle unexported fields; use IgnoreUnexported instead.
|
||||||
|
func IgnoreFields(typ interface{}, names ...string) cmp.Option {
|
||||||
|
sf := newStructFilter(typ, names...)
|
||||||
|
return cmp.FilterPath(sf.filter, cmp.Ignore())
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoreTypes returns an Option that ignores all values assignable to
|
||||||
|
// certain types, which are specified by passing in a value of each type.
|
||||||
|
func IgnoreTypes(typs ...interface{}) cmp.Option {
|
||||||
|
tf := newTypeFilter(typs...)
|
||||||
|
return cmp.FilterPath(tf.filter, cmp.Ignore())
|
||||||
|
}
|
||||||
|
|
||||||
|
type typeFilter []reflect.Type
|
||||||
|
|
||||||
|
func newTypeFilter(typs ...interface{}) (tf typeFilter) {
|
||||||
|
for _, typ := range typs {
|
||||||
|
t := reflect.TypeOf(typ)
|
||||||
|
if t == nil {
|
||||||
|
// This occurs if someone tries to pass in sync.Locker(nil)
|
||||||
|
panic("cannot determine type; consider using IgnoreInterfaces")
|
||||||
|
}
|
||||||
|
tf = append(tf, t)
|
||||||
|
}
|
||||||
|
return tf
|
||||||
|
}
|
||||||
|
func (tf typeFilter) filter(p cmp.Path) bool {
|
||||||
|
if len(p) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t := p.Last().Type()
|
||||||
|
for _, ti := range tf {
|
||||||
|
if t.AssignableTo(ti) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoreInterfaces returns an Option that ignores all values or references of
|
||||||
|
// values assignable to certain interface types. These interfaces are specified
|
||||||
|
// by passing in an anonymous struct with the interface types embedded in it.
|
||||||
|
// For example, to ignore sync.Locker, pass in struct{sync.Locker}{}.
|
||||||
|
func IgnoreInterfaces(ifaces interface{}) cmp.Option {
|
||||||
|
tf := newIfaceFilter(ifaces)
|
||||||
|
return cmp.FilterPath(tf.filter, cmp.Ignore())
|
||||||
|
}
|
||||||
|
|
||||||
|
type ifaceFilter []reflect.Type
|
||||||
|
|
||||||
|
func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) {
|
||||||
|
t := reflect.TypeOf(ifaces)
|
||||||
|
if ifaces == nil || t.Name() != "" || t.Kind() != reflect.Struct {
|
||||||
|
panic("input must be an anonymous struct")
|
||||||
|
}
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
fi := t.Field(i)
|
||||||
|
switch {
|
||||||
|
case !fi.Anonymous:
|
||||||
|
panic("struct cannot have named fields")
|
||||||
|
case fi.Type.Kind() != reflect.Interface:
|
||||||
|
panic("embedded field must be an interface type")
|
||||||
|
case fi.Type.NumMethod() == 0:
|
||||||
|
// This matches everything; why would you ever want this?
|
||||||
|
panic("cannot ignore empty interface")
|
||||||
|
default:
|
||||||
|
tf = append(tf, fi.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tf
|
||||||
|
}
|
||||||
|
func (tf ifaceFilter) filter(p cmp.Path) bool {
|
||||||
|
if len(p) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t := p.Last().Type()
|
||||||
|
for _, ti := range tf {
|
||||||
|
if t.AssignableTo(ti) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if t.Kind() != reflect.Ptr && reflect.PtrTo(t).AssignableTo(ti) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoreUnexported returns an Option that only ignores the immediate unexported
|
||||||
|
// fields of a struct, including anonymous fields of unexported types.
|
||||||
|
// In particular, unexported fields within the struct's exported fields
|
||||||
|
// of struct types, including anonymous fields, will not be ignored unless the
|
||||||
|
// type of the field itself is also passed to IgnoreUnexported.
|
||||||
|
//
|
||||||
|
// Avoid ignoring unexported fields of a type which you do not control (i.e. a
|
||||||
|
// type from another repository), as changes to the implementation of such types
|
||||||
|
// may change how the comparison behaves. Prefer a custom Comparer instead.
|
||||||
|
func IgnoreUnexported(typs ...interface{}) cmp.Option {
|
||||||
|
ux := newUnexportedFilter(typs...)
|
||||||
|
return cmp.FilterPath(ux.filter, cmp.Ignore())
|
||||||
|
}
|
||||||
|
|
||||||
|
type unexportedFilter struct{ m map[reflect.Type]bool }
|
||||||
|
|
||||||
|
func newUnexportedFilter(typs ...interface{}) unexportedFilter {
|
||||||
|
ux := unexportedFilter{m: make(map[reflect.Type]bool)}
|
||||||
|
for _, typ := range typs {
|
||||||
|
t := reflect.TypeOf(typ)
|
||||||
|
if t == nil || t.Kind() != reflect.Struct {
|
||||||
|
panic(fmt.Sprintf("invalid struct type: %T", typ))
|
||||||
|
}
|
||||||
|
ux.m[t] = true
|
||||||
|
}
|
||||||
|
return ux
|
||||||
|
}
|
||||||
|
func (xf unexportedFilter) filter(p cmp.Path) bool {
|
||||||
|
sf, ok := p.Index(-1).(cmp.StructField)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return xf.m[p.Index(-2).Type()] && !isExported(sf.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// isExported reports whether the identifier is exported.
|
||||||
|
func isExported(id string) bool {
|
||||||
|
r, _ := utf8.DecodeRuneInString(id)
|
||||||
|
return unicode.IsUpper(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoreSliceElements returns an Option that ignores elements of []V.
|
||||||
|
// The discard function must be of the form "func(T) bool" which is used to
|
||||||
|
// ignore slice elements of type V, where V is assignable to T.
|
||||||
|
// Elements are ignored if the function reports true.
|
||||||
|
func IgnoreSliceElements(discardFunc interface{}) cmp.Option {
|
||||||
|
vf := reflect.ValueOf(discardFunc)
|
||||||
|
if !function.IsType(vf.Type(), function.ValuePredicate) || vf.IsNil() {
|
||||||
|
panic(fmt.Sprintf("invalid discard function: %T", discardFunc))
|
||||||
|
}
|
||||||
|
return cmp.FilterPath(func(p cmp.Path) bool {
|
||||||
|
si, ok := p.Index(-1).(cmp.SliceIndex)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !si.Type().AssignableTo(vf.Type().In(0)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
vx, vy := si.Values()
|
||||||
|
if vx.IsValid() && vf.Call([]reflect.Value{vx})[0].Bool() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if vy.IsValid() && vf.Call([]reflect.Value{vy})[0].Bool() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, cmp.Ignore())
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoreMapEntries returns an Option that ignores entries of map[K]V.
|
||||||
|
// The discard function must be of the form "func(T, R) bool" which is used to
|
||||||
|
// ignore map entries of type K and V, where K and V are assignable to T and R.
|
||||||
|
// Entries are ignored if the function reports true.
|
||||||
|
func IgnoreMapEntries(discardFunc interface{}) cmp.Option {
|
||||||
|
vf := reflect.ValueOf(discardFunc)
|
||||||
|
if !function.IsType(vf.Type(), function.KeyValuePredicate) || vf.IsNil() {
|
||||||
|
panic(fmt.Sprintf("invalid discard function: %T", discardFunc))
|
||||||
|
}
|
||||||
|
return cmp.FilterPath(func(p cmp.Path) bool {
|
||||||
|
mi, ok := p.Index(-1).(cmp.MapIndex)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !mi.Key().Type().AssignableTo(vf.Type().In(0)) || !mi.Type().AssignableTo(vf.Type().In(1)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
k := mi.Key()
|
||||||
|
vx, vy := mi.Values()
|
||||||
|
if vx.IsValid() && vf.Call([]reflect.Value{k, vx})[0].Bool() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if vy.IsValid() && vf.Call([]reflect.Value{k, vy})[0].Bool() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, cmp.Ignore())
|
||||||
|
}
|
147
vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go
generated
vendored
Normal file
147
vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go
generated
vendored
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE.md file.
|
||||||
|
|
||||||
|
package cmpopts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/internal/function"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SortSlices returns a Transformer option that sorts all []V.
|
||||||
|
// The less function must be of the form "func(T, T) bool" which is used to
|
||||||
|
// sort any slice with element type V that is assignable to T.
|
||||||
|
//
|
||||||
|
// The less function must be:
|
||||||
|
// • Deterministic: less(x, y) == less(x, y)
|
||||||
|
// • Irreflexive: !less(x, x)
|
||||||
|
// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z)
|
||||||
|
//
|
||||||
|
// The less function does not have to be "total". That is, if !less(x, y) and
|
||||||
|
// !less(y, x) for two elements x and y, their relative order is maintained.
|
||||||
|
//
|
||||||
|
// SortSlices can be used in conjunction with EquateEmpty.
|
||||||
|
func SortSlices(lessFunc interface{}) cmp.Option {
|
||||||
|
vf := reflect.ValueOf(lessFunc)
|
||||||
|
if !function.IsType(vf.Type(), function.Less) || vf.IsNil() {
|
||||||
|
panic(fmt.Sprintf("invalid less function: %T", lessFunc))
|
||||||
|
}
|
||||||
|
ss := sliceSorter{vf.Type().In(0), vf}
|
||||||
|
return cmp.FilterValues(ss.filter, cmp.Transformer("cmpopts.SortSlices", ss.sort))
|
||||||
|
}
|
||||||
|
|
||||||
|
type sliceSorter struct {
|
||||||
|
in reflect.Type // T
|
||||||
|
fnc reflect.Value // func(T, T) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss sliceSorter) filter(x, y interface{}) bool {
|
||||||
|
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||||
|
if !(x != nil && y != nil && vx.Type() == vy.Type()) ||
|
||||||
|
!(vx.Kind() == reflect.Slice && vx.Type().Elem().AssignableTo(ss.in)) ||
|
||||||
|
(vx.Len() <= 1 && vy.Len() <= 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Check whether the slices are already sorted to avoid an infinite
|
||||||
|
// recursion cycle applying the same transform to itself.
|
||||||
|
ok1 := sort.SliceIsSorted(x, func(i, j int) bool { return ss.less(vx, i, j) })
|
||||||
|
ok2 := sort.SliceIsSorted(y, func(i, j int) bool { return ss.less(vy, i, j) })
|
||||||
|
return !ok1 || !ok2
|
||||||
|
}
|
||||||
|
func (ss sliceSorter) sort(x interface{}) interface{} {
|
||||||
|
src := reflect.ValueOf(x)
|
||||||
|
dst := reflect.MakeSlice(src.Type(), src.Len(), src.Len())
|
||||||
|
for i := 0; i < src.Len(); i++ {
|
||||||
|
dst.Index(i).Set(src.Index(i))
|
||||||
|
}
|
||||||
|
sort.SliceStable(dst.Interface(), func(i, j int) bool { return ss.less(dst, i, j) })
|
||||||
|
ss.checkSort(dst)
|
||||||
|
return dst.Interface()
|
||||||
|
}
|
||||||
|
func (ss sliceSorter) checkSort(v reflect.Value) {
|
||||||
|
start := -1 // Start of a sequence of equal elements.
|
||||||
|
for i := 1; i < v.Len(); i++ {
|
||||||
|
if ss.less(v, i-1, i) {
|
||||||
|
// Check that first and last elements in v[start:i] are equal.
|
||||||
|
if start >= 0 && (ss.less(v, start, i-1) || ss.less(v, i-1, start)) {
|
||||||
|
panic(fmt.Sprintf("incomparable values detected: want equal elements: %v", v.Slice(start, i)))
|
||||||
|
}
|
||||||
|
start = -1
|
||||||
|
} else if start == -1 {
|
||||||
|
start = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (ss sliceSorter) less(v reflect.Value, i, j int) bool {
|
||||||
|
vx, vy := v.Index(i), v.Index(j)
|
||||||
|
return ss.fnc.Call([]reflect.Value{vx, vy})[0].Bool()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortMaps returns a Transformer option that flattens map[K]V types to be a
|
||||||
|
// sorted []struct{K, V}. The less function must be of the form
|
||||||
|
// "func(T, T) bool" which is used to sort any map with key K that is
|
||||||
|
// assignable to T.
|
||||||
|
//
|
||||||
|
// Flattening the map into a slice has the property that cmp.Equal is able to
|
||||||
|
// use Comparers on K or the K.Equal method if it exists.
|
||||||
|
//
|
||||||
|
// The less function must be:
|
||||||
|
// • Deterministic: less(x, y) == less(x, y)
|
||||||
|
// • Irreflexive: !less(x, x)
|
||||||
|
// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z)
|
||||||
|
// • Total: if x != y, then either less(x, y) or less(y, x)
|
||||||
|
//
|
||||||
|
// SortMaps can be used in conjunction with EquateEmpty.
|
||||||
|
func SortMaps(lessFunc interface{}) cmp.Option {
|
||||||
|
vf := reflect.ValueOf(lessFunc)
|
||||||
|
if !function.IsType(vf.Type(), function.Less) || vf.IsNil() {
|
||||||
|
panic(fmt.Sprintf("invalid less function: %T", lessFunc))
|
||||||
|
}
|
||||||
|
ms := mapSorter{vf.Type().In(0), vf}
|
||||||
|
return cmp.FilterValues(ms.filter, cmp.Transformer("cmpopts.SortMaps", ms.sort))
|
||||||
|
}
|
||||||
|
|
||||||
|
type mapSorter struct {
|
||||||
|
in reflect.Type // T
|
||||||
|
fnc reflect.Value // func(T, T) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms mapSorter) filter(x, y interface{}) bool {
|
||||||
|
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||||
|
return (x != nil && y != nil && vx.Type() == vy.Type()) &&
|
||||||
|
(vx.Kind() == reflect.Map && vx.Type().Key().AssignableTo(ms.in)) &&
|
||||||
|
(vx.Len() != 0 || vy.Len() != 0)
|
||||||
|
}
|
||||||
|
func (ms mapSorter) sort(x interface{}) interface{} {
|
||||||
|
src := reflect.ValueOf(x)
|
||||||
|
outType := reflect.StructOf([]reflect.StructField{
|
||||||
|
{Name: "K", Type: src.Type().Key()},
|
||||||
|
{Name: "V", Type: src.Type().Elem()},
|
||||||
|
})
|
||||||
|
dst := reflect.MakeSlice(reflect.SliceOf(outType), src.Len(), src.Len())
|
||||||
|
for i, k := range src.MapKeys() {
|
||||||
|
v := reflect.New(outType).Elem()
|
||||||
|
v.Field(0).Set(k)
|
||||||
|
v.Field(1).Set(src.MapIndex(k))
|
||||||
|
dst.Index(i).Set(v)
|
||||||
|
}
|
||||||
|
sort.Slice(dst.Interface(), func(i, j int) bool { return ms.less(dst, i, j) })
|
||||||
|
ms.checkSort(dst)
|
||||||
|
return dst.Interface()
|
||||||
|
}
|
||||||
|
func (ms mapSorter) checkSort(v reflect.Value) {
|
||||||
|
for i := 1; i < v.Len(); i++ {
|
||||||
|
if !ms.less(v, i-1, i) {
|
||||||
|
panic(fmt.Sprintf("partial order detected: want %v < %v", v.Index(i-1), v.Index(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (ms mapSorter) less(v reflect.Value, i, j int) bool {
|
||||||
|
vx, vy := v.Index(i).Field(0), v.Index(j).Field(0)
|
||||||
|
return ms.fnc.Call([]reflect.Value{vx, vy})[0].Bool()
|
||||||
|
}
|
182
vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go
generated
vendored
Normal file
182
vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go
generated
vendored
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE.md file.
|
||||||
|
|
||||||
|
package cmpopts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// filterField returns a new Option where opt is only evaluated on paths that
|
||||||
|
// include a specific exported field on a single struct type.
|
||||||
|
// The struct type is specified by passing in a value of that type.
|
||||||
|
//
|
||||||
|
// The name may be a dot-delimited string (e.g., "Foo.Bar") to select a
|
||||||
|
// specific sub-field that is embedded or nested within the parent struct.
|
||||||
|
func filterField(typ interface{}, name string, opt cmp.Option) cmp.Option {
|
||||||
|
// TODO: This is currently unexported over concerns of how helper filters
|
||||||
|
// can be composed together easily.
|
||||||
|
// TODO: Add tests for FilterField.
|
||||||
|
|
||||||
|
sf := newStructFilter(typ, name)
|
||||||
|
return cmp.FilterPath(sf.filter, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
type structFilter struct {
|
||||||
|
t reflect.Type // The root struct type to match on
|
||||||
|
ft fieldTree // Tree of fields to match on
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStructFilter(typ interface{}, names ...string) structFilter {
|
||||||
|
// TODO: Perhaps allow * as a special identifier to allow ignoring any
|
||||||
|
// number of path steps until the next field match?
|
||||||
|
// This could be useful when a concrete struct gets transformed into
|
||||||
|
// an anonymous struct where it is not possible to specify that by type,
|
||||||
|
// but the transformer happens to provide guarantees about the names of
|
||||||
|
// the transformed fields.
|
||||||
|
|
||||||
|
t := reflect.TypeOf(typ)
|
||||||
|
if t == nil || t.Kind() != reflect.Struct {
|
||||||
|
panic(fmt.Sprintf("%T must be a struct", typ))
|
||||||
|
}
|
||||||
|
var ft fieldTree
|
||||||
|
for _, name := range names {
|
||||||
|
cname, err := canonicalName(t, name)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("%s: %v", strings.Join(cname, "."), err))
|
||||||
|
}
|
||||||
|
ft.insert(cname)
|
||||||
|
}
|
||||||
|
return structFilter{t, ft}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sf structFilter) filter(p cmp.Path) bool {
|
||||||
|
for i, ps := range p {
|
||||||
|
if ps.Type().AssignableTo(sf.t) && sf.ft.matchPrefix(p[i+1:]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldTree represents a set of dot-separated identifiers.
|
||||||
|
//
|
||||||
|
// For example, inserting the following selectors:
|
||||||
|
// Foo
|
||||||
|
// Foo.Bar.Baz
|
||||||
|
// Foo.Buzz
|
||||||
|
// Nuka.Cola.Quantum
|
||||||
|
//
|
||||||
|
// Results in a tree of the form:
|
||||||
|
// {sub: {
|
||||||
|
// "Foo": {ok: true, sub: {
|
||||||
|
// "Bar": {sub: {
|
||||||
|
// "Baz": {ok: true},
|
||||||
|
// }},
|
||||||
|
// "Buzz": {ok: true},
|
||||||
|
// }},
|
||||||
|
// "Nuka": {sub: {
|
||||||
|
// "Cola": {sub: {
|
||||||
|
// "Quantum": {ok: true},
|
||||||
|
// }},
|
||||||
|
// }},
|
||||||
|
// }}
|
||||||
|
type fieldTree struct {
|
||||||
|
ok bool // Whether this is a specified node
|
||||||
|
sub map[string]fieldTree // The sub-tree of fields under this node
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert inserts a sequence of field accesses into the tree.
|
||||||
|
func (ft *fieldTree) insert(cname []string) {
|
||||||
|
if ft.sub == nil {
|
||||||
|
ft.sub = make(map[string]fieldTree)
|
||||||
|
}
|
||||||
|
if len(cname) == 0 {
|
||||||
|
ft.ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sub := ft.sub[cname[0]]
|
||||||
|
sub.insert(cname[1:])
|
||||||
|
ft.sub[cname[0]] = sub
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchPrefix reports whether any selector in the fieldTree matches
|
||||||
|
// the start of path p.
|
||||||
|
func (ft fieldTree) matchPrefix(p cmp.Path) bool {
|
||||||
|
for _, ps := range p {
|
||||||
|
switch ps := ps.(type) {
|
||||||
|
case cmp.StructField:
|
||||||
|
ft = ft.sub[ps.Name()]
|
||||||
|
if ft.ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(ft.sub) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case cmp.Indirect:
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// canonicalName returns a list of identifiers where any struct field access
|
||||||
|
// through an embedded field is expanded to include the names of the embedded
|
||||||
|
// types themselves.
|
||||||
|
//
|
||||||
|
// For example, suppose field "Foo" is not directly in the parent struct,
|
||||||
|
// but actually from an embedded struct of type "Bar". Then, the canonical name
|
||||||
|
// of "Foo" is actually "Bar.Foo".
|
||||||
|
//
|
||||||
|
// Suppose field "Foo" is not directly in the parent struct, but actually
|
||||||
|
// a field in two different embedded structs of types "Bar" and "Baz".
|
||||||
|
// Then the selector "Foo" causes a panic since it is ambiguous which one it
|
||||||
|
// refers to. The user must specify either "Bar.Foo" or "Baz.Foo".
|
||||||
|
func canonicalName(t reflect.Type, sel string) ([]string, error) {
|
||||||
|
var name string
|
||||||
|
sel = strings.TrimPrefix(sel, ".")
|
||||||
|
if sel == "" {
|
||||||
|
return nil, fmt.Errorf("name must not be empty")
|
||||||
|
}
|
||||||
|
if i := strings.IndexByte(sel, '.'); i < 0 {
|
||||||
|
name, sel = sel, ""
|
||||||
|
} else {
|
||||||
|
name, sel = sel[:i], sel[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type must be a struct or pointer to struct.
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("%v must be a struct", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the canonical name for this current field name.
|
||||||
|
// If the field exists in an embedded struct, then it will be expanded.
|
||||||
|
if !isExported(name) {
|
||||||
|
// Disallow unexported fields:
|
||||||
|
// * To discourage people from actually touching unexported fields
|
||||||
|
// * FieldByName is buggy (https://golang.org/issue/4876)
|
||||||
|
return []string{name}, fmt.Errorf("name must be exported")
|
||||||
|
}
|
||||||
|
sf, ok := t.FieldByName(name)
|
||||||
|
if !ok {
|
||||||
|
return []string{name}, fmt.Errorf("does not exist")
|
||||||
|
}
|
||||||
|
var ss []string
|
||||||
|
for i := range sf.Index {
|
||||||
|
ss = append(ss, t.FieldByIndex(sf.Index[:i+1]).Name)
|
||||||
|
}
|
||||||
|
if sel == "" {
|
||||||
|
return ss, nil
|
||||||
|
}
|
||||||
|
ssPost, err := canonicalName(sf.Type, sel)
|
||||||
|
return append(ss, ssPost...), err
|
||||||
|
}
|
35
vendor/github.com/google/go-cmp/cmp/cmpopts/xform.go
generated
vendored
Normal file
35
vendor/github.com/google/go-cmp/cmp/cmpopts/xform.go
generated
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2018, The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE.md file.
|
||||||
|
|
||||||
|
package cmpopts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type xformFilter struct{ xform cmp.Option }
|
||||||
|
|
||||||
|
func (xf xformFilter) filter(p cmp.Path) bool {
|
||||||
|
for _, ps := range p {
|
||||||
|
if t, ok := ps.(cmp.Transform); ok && t.Option() == xf.xform {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcyclicTransformer returns a Transformer with a filter applied that ensures
|
||||||
|
// that the transformer cannot be recursively applied upon its own output.
|
||||||
|
//
|
||||||
|
// An example use case is a transformer that splits a string by lines:
|
||||||
|
// AcyclicTransformer("SplitLines", func(s string) []string{
|
||||||
|
// return strings.Split(s, "\n")
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// Had this been an unfiltered Transformer instead, this would result in an
|
||||||
|
// infinite cycle converting a string to []string to [][]string and so on.
|
||||||
|
func AcyclicTransformer(name string, xformFunc interface{}) cmp.Option {
|
||||||
|
xf := xformFilter{cmp.Transformer(name, xformFunc)}
|
||||||
|
return cmp.FilterPath(xf.filter, xf.xform)
|
||||||
|
}
|
59
vendor/github.com/grafana/grafana-plugin-sdk-go/data/frame.go
generated
vendored
59
vendor/github.com/grafana/grafana-plugin-sdk-go/data/frame.go
generated
vendored
@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Frame represents a columnar storage with optional labels.
|
// Frame represents a columnar storage with optional labels.
|
||||||
@ -45,6 +46,15 @@ func (f *Frame) AppendRow(vals ...interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RowCopy returns an interface slice that contains the values of each Field for the given rowIdx.
|
||||||
|
func (f *Frame) RowCopy(rowIdx int) []interface{} {
|
||||||
|
vals := make([]interface{}, len(f.Fields))
|
||||||
|
for i := range f.Fields {
|
||||||
|
vals[i] = f.CopyAt(i, rowIdx)
|
||||||
|
}
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
// AppendWarning adds warnings to the data frame.
|
// AppendWarning adds warnings to the data frame.
|
||||||
func (f *Frame) AppendWarning(message string, details string) {
|
func (f *Frame) AppendWarning(message string, details string) {
|
||||||
f.Warnings = append(f.Warnings, Warning{Message: message, Details: details})
|
f.Warnings = append(f.Warnings, Warning{Message: message, Details: details})
|
||||||
@ -76,6 +86,44 @@ func (f *Frame) AppendRowSafe(vals ...interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FilterRowsByField returns a copy of frame f (as per EmptyCopy()) that includes rows
|
||||||
|
// where the filter returns true and no error. If filter returns an error, then an error is returned.
|
||||||
|
func (f *Frame) FilterRowsByField(fieldIdx int, filter func(i interface{}) (bool, error)) (*Frame, error) {
|
||||||
|
filteredFrame := f.EmptyCopy()
|
||||||
|
rowLen, err := f.RowLen()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for inRowIdx := 0; inRowIdx < rowLen; inRowIdx++ {
|
||||||
|
match, err := filter(f.At(fieldIdx, inRowIdx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filteredFrame.AppendRow(f.RowCopy(inRowIdx)...)
|
||||||
|
}
|
||||||
|
return filteredFrame, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyCopy returns a copy of Frame f but with Fields of zero length, and no copy of the FieldConfigs, Metadata, or Warnings.
|
||||||
|
func (f *Frame) EmptyCopy() *Frame {
|
||||||
|
newFrame := &Frame{
|
||||||
|
Name: f.Name,
|
||||||
|
RefID: f.RefID,
|
||||||
|
Fields: make(Fields, 0, len(f.Fields)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range f.Fields {
|
||||||
|
copy := NewFieldFromFieldType(field.Type(), 0)
|
||||||
|
copy.Name = field.Name
|
||||||
|
copy.Labels = field.Labels.Copy()
|
||||||
|
newFrame.Fields = append(newFrame.Fields, copy)
|
||||||
|
}
|
||||||
|
return newFrame
|
||||||
|
}
|
||||||
|
|
||||||
// TypeIndices returns a slice of Field index positions for the given pTypes.
|
// TypeIndices returns a slice of Field index positions for the given pTypes.
|
||||||
func (f *Frame) TypeIndices(pTypes ...FieldType) []int {
|
func (f *Frame) TypeIndices(pTypes ...FieldType) []int {
|
||||||
indices := []int{}
|
indices := []int{}
|
||||||
@ -319,6 +367,15 @@ func (l Labels) Equals(arg Labels) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy returns a copy of the labels.
|
||||||
|
func (l Labels) Copy() Labels {
|
||||||
|
c := make(Labels, len(l))
|
||||||
|
for k, v := range l {
|
||||||
|
c[k] = v
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
// Contains returns true if all k=v pairs of the argument are in the receiver.
|
// Contains returns true if all k=v pairs of the argument are in the receiver.
|
||||||
func (l Labels) Contains(arg Labels) bool {
|
func (l Labels) Contains(arg Labels) bool {
|
||||||
if len(arg) > len(l) {
|
if len(arg) > len(l) {
|
||||||
@ -483,5 +540,5 @@ func FrameTestCompareOptions() []cmp.Option {
|
|||||||
})
|
})
|
||||||
|
|
||||||
unexportedField := cmp.AllowUnexported(Field{})
|
unexportedField := cmp.AllowUnexported(Field{})
|
||||||
return []cmp.Option{confFloats, unexportedField}
|
return []cmp.Option{confFloats, unexportedField, cmpopts.EquateEmpty()}
|
||||||
}
|
}
|
||||||
|
3
vendor/modules.txt
vendored
3
vendor/modules.txt
vendored
@ -134,6 +134,7 @@ github.com/golang/snappy
|
|||||||
github.com/google/flatbuffers/go
|
github.com/google/flatbuffers/go
|
||||||
# github.com/google/go-cmp v0.3.1
|
# github.com/google/go-cmp v0.3.1
|
||||||
github.com/google/go-cmp/cmp
|
github.com/google/go-cmp/cmp
|
||||||
|
github.com/google/go-cmp/cmp/cmpopts
|
||||||
github.com/google/go-cmp/cmp/internal/diff
|
github.com/google/go-cmp/cmp/internal/diff
|
||||||
github.com/google/go-cmp/cmp/internal/flags
|
github.com/google/go-cmp/cmp/internal/flags
|
||||||
github.com/google/go-cmp/cmp/internal/function
|
github.com/google/go-cmp/cmp/internal/function
|
||||||
@ -147,7 +148,7 @@ github.com/gosimple/slug
|
|||||||
# github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4
|
# github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4
|
||||||
github.com/grafana/grafana-plugin-model/go/datasource
|
github.com/grafana/grafana-plugin-model/go/datasource
|
||||||
github.com/grafana/grafana-plugin-model/go/renderer
|
github.com/grafana/grafana-plugin-model/go/renderer
|
||||||
# github.com/grafana/grafana-plugin-sdk-go v0.26.0
|
# github.com/grafana/grafana-plugin-sdk-go v0.28.0
|
||||||
github.com/grafana/grafana-plugin-sdk-go/backend/plugin
|
github.com/grafana/grafana-plugin-sdk-go/backend/plugin
|
||||||
github.com/grafana/grafana-plugin-sdk-go/data
|
github.com/grafana/grafana-plugin-sdk-go/data
|
||||||
github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2
|
github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2
|
||||||
|
Reference in New Issue
Block a user