2021-03-08 00:02:49 -06:00
|
|
|
package manager
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2021-11-01 04:53:33 -05:00
|
|
|
"io"
|
2021-03-08 00:02:49 -06:00
|
|
|
"io/ioutil"
|
2021-11-01 04:53:33 -05:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2021-03-08 00:02:49 -06:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
2021-05-12 13:05:16 -05:00
|
|
|
"sync"
|
2021-03-08 00:02:49 -06:00
|
|
|
"time"
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
|
|
|
2021-03-08 00:02:49 -06:00
|
|
|
"github.com/grafana/grafana/pkg/infra/fs"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
|
|
"github.com/grafana/grafana/pkg/models"
|
|
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
|
|
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
2021-11-01 04:53:33 -05:00
|
|
|
"github.com/grafana/grafana/pkg/plugins/backendplugin/instrumentation"
|
2021-05-12 13:05:16 -05:00
|
|
|
"github.com/grafana/grafana/pkg/plugins/manager/installer"
|
2021-03-17 10:06:10 -05:00
|
|
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
2021-03-08 00:02:49 -06:00
|
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
|
|
"github.com/grafana/grafana/pkg/util/errutil"
|
2021-11-01 04:53:33 -05:00
|
|
|
"github.com/grafana/grafana/pkg/util/proxyutil"
|
2021-05-12 13:05:16 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
grafanaComURL = "https://grafana.com/api/plugins"
|
2021-03-08 00:02:49 -06:00
|
|
|
)
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
var _ plugins.Client = (*PluginManager)(nil)
|
|
|
|
var _ plugins.Store = (*PluginManager)(nil)
|
|
|
|
var _ plugins.PluginDashboardManager = (*PluginManager)(nil)
|
|
|
|
var _ plugins.StaticRouteResolver = (*PluginManager)(nil)
|
|
|
|
var _ plugins.CoreBackendRegistrar = (*PluginManager)(nil)
|
|
|
|
var _ plugins.RendererManager = (*PluginManager)(nil)
|
2021-03-08 00:02:49 -06:00
|
|
|
|
|
|
|
type PluginManager struct {
|
2021-11-01 04:53:33 -05:00
|
|
|
cfg *setting.Cfg
|
|
|
|
requestValidator models.PluginRequestValidator
|
|
|
|
sqlStore *sqlstore.SQLStore
|
|
|
|
plugins map[string]*plugins.Plugin
|
|
|
|
pluginInstaller plugins.Installer
|
|
|
|
pluginLoader plugins.Loader
|
|
|
|
pluginsMu sync.RWMutex
|
|
|
|
log log.Logger
|
|
|
|
}
|
|
|
|
|
|
|
|
func ProvideService(cfg *setting.Cfg, requestValidator models.PluginRequestValidator, pluginLoader plugins.Loader,
|
|
|
|
sqlStore *sqlstore.SQLStore) (*PluginManager, error) {
|
|
|
|
pm := newManager(cfg, requestValidator, pluginLoader, sqlStore)
|
2021-08-25 08:11:22 -05:00
|
|
|
if err := pm.init(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return pm, nil
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func newManager(cfg *setting.Cfg, pluginRequestValidator models.PluginRequestValidator, pluginLoader plugins.Loader,
|
|
|
|
sqlStore *sqlstore.SQLStore) *PluginManager {
|
2021-03-18 07:53:01 -05:00
|
|
|
return &PluginManager{
|
2021-11-01 04:53:33 -05:00
|
|
|
cfg: cfg,
|
|
|
|
requestValidator: pluginRequestValidator,
|
|
|
|
sqlStore: sqlStore,
|
|
|
|
pluginLoader: pluginLoader,
|
|
|
|
plugins: map[string]*plugins.Plugin{},
|
|
|
|
log: log.New("plugin.manager"),
|
|
|
|
pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)),
|
2021-03-18 07:53:01 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) init() error {
|
|
|
|
// create external plugin's path if not exists
|
|
|
|
exists, err := fs.Exists(m.cfg.PluginsPath)
|
2021-03-08 00:02:49 -06:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-11-01 04:53:33 -05:00
|
|
|
|
|
|
|
if !exists {
|
|
|
|
if err = os.MkdirAll(m.cfg.PluginsPath, os.ModePerm); err != nil {
|
|
|
|
m.log.Error("Failed to create external plugins directory", "dir", m.cfg.PluginsPath, "error", err)
|
|
|
|
} else {
|
|
|
|
m.log.Debug("External plugins directory created", "dir", m.cfg.PluginsPath)
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
m.log.Info("Initialising plugins")
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// install Core plugins
|
|
|
|
err = m.loadPlugins(m.corePluginPaths()...)
|
2021-03-08 00:02:49 -06:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// install Bundled plugins
|
|
|
|
err = m.loadPlugins(m.cfg.BundledPluginsPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// install External plugins
|
|
|
|
err = m.loadPlugins(m.cfg.PluginsPath)
|
|
|
|
if err != nil {
|
2021-03-08 00:02:49 -06:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// install plugins from cfg.PluginSettings
|
|
|
|
err = m.loadPlugins(m.pluginSettingPaths()...)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *PluginManager) Run(ctx context.Context) error {
|
|
|
|
if m.cfg.CheckForUpdates {
|
|
|
|
go func() {
|
|
|
|
m.checkForUpdates()
|
|
|
|
|
|
|
|
ticker := time.NewTicker(time.Minute * 10)
|
|
|
|
run := true
|
|
|
|
|
|
|
|
for run {
|
|
|
|
select {
|
|
|
|
case <-ticker.C:
|
|
|
|
m.checkForUpdates()
|
|
|
|
case <-ctx.Done():
|
|
|
|
run = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
<-ctx.Done()
|
|
|
|
m.stop(ctx)
|
|
|
|
return ctx.Err()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *PluginManager) loadPlugins(paths ...string) error {
|
|
|
|
if len(paths) == 0 {
|
|
|
|
return nil
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
var pluginPaths []string
|
|
|
|
for _, p := range paths {
|
|
|
|
if p != "" {
|
|
|
|
pluginPaths = append(pluginPaths, p)
|
|
|
|
}
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
loadedPlugins, err := m.pluginLoader.Load(pluginPaths, m.registeredPlugins())
|
|
|
|
if err != nil {
|
|
|
|
m.log.Error("Could not load plugins", "paths", pluginPaths, "err", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, p := range loadedPlugins {
|
|
|
|
if err := m.registerAndStart(context.Background(), p); err != nil {
|
|
|
|
m.log.Error("Could not start plugin", "pluginId", p.ID, "err", err)
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) registeredPlugins() map[string]struct{} {
|
|
|
|
pluginsByID := make(map[string]struct{})
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
m.pluginsMu.RLock()
|
|
|
|
defer m.pluginsMu.RUnlock()
|
|
|
|
for _, p := range m.plugins {
|
|
|
|
pluginsByID[p.ID] = struct{}{}
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return pluginsByID
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) Plugin(pluginID string) *plugins.Plugin {
|
|
|
|
m.pluginsMu.RLock()
|
|
|
|
p, ok := m.plugins[pluginID]
|
|
|
|
m.pluginsMu.RUnlock()
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if ok && (p.IsDecommissioned()) {
|
|
|
|
return nil
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return p
|
2021-03-17 10:06:10 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) Plugins(pluginTypes ...plugins.Type) []*plugins.Plugin {
|
|
|
|
// if no types passed, assume all
|
|
|
|
if len(pluginTypes) == 0 {
|
|
|
|
pluginTypes = plugins.PluginTypes
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
var requestedTypes = make(map[plugins.Type]struct{})
|
|
|
|
for _, pt := range pluginTypes {
|
|
|
|
requestedTypes[pt] = struct{}{}
|
2021-03-17 10:06:10 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
m.pluginsMu.RLock()
|
|
|
|
var pluginsList []*plugins.Plugin
|
|
|
|
for _, p := range m.plugins {
|
|
|
|
if _, exists := requestedTypes[p.Type]; exists {
|
|
|
|
pluginsList = append(pluginsList, p)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
m.pluginsMu.RUnlock()
|
|
|
|
return pluginsList
|
2021-03-17 10:06:10 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) Renderer() *plugins.Plugin {
|
|
|
|
for _, p := range m.plugins {
|
|
|
|
if p.IsRenderer() {
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return nil
|
2021-03-17 10:06:10 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
|
|
|
plugin := m.Plugin(req.PluginContext.PluginID)
|
|
|
|
if plugin == nil {
|
2021-11-10 03:39:32 -06:00
|
|
|
return nil, backendplugin.ErrPluginNotRegistered
|
2021-11-01 04:53:33 -05:00
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
var resp *backend.QueryDataResponse
|
|
|
|
err := instrumentation.InstrumentQueryDataRequest(req.PluginContext.PluginID, func() (innerErr error) {
|
|
|
|
resp, innerErr = plugin.QueryData(ctx, req)
|
|
|
|
return
|
|
|
|
})
|
2021-03-18 07:53:01 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-03-18 07:53:01 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return nil, errutil.Wrap("failed to query data", err)
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
for refID, res := range resp.Responses {
|
|
|
|
// set frame ref ID based on response ref ID
|
|
|
|
for _, f := range res.Frames {
|
|
|
|
if f.RefID == "" {
|
|
|
|
f.RefID = refID
|
|
|
|
}
|
|
|
|
}
|
2021-03-17 10:06:10 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return resp, err
|
2021-03-17 10:06:10 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) CallResource(pCtx backend.PluginContext, reqCtx *models.ReqContext, path string) {
|
|
|
|
var dsURL string
|
|
|
|
if pCtx.DataSourceInstanceSettings != nil {
|
|
|
|
dsURL = pCtx.DataSourceInstanceSettings.URL
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
err := m.requestValidator.Validate(dsURL, reqCtx.Req)
|
|
|
|
if err != nil {
|
|
|
|
reqCtx.JsonApiErr(http.StatusForbidden, "Access denied", err)
|
|
|
|
return
|
2021-03-18 07:53:01 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
clonedReq := reqCtx.Req.Clone(reqCtx.Req.Context())
|
|
|
|
rawURL := path
|
|
|
|
if clonedReq.URL.RawQuery != "" {
|
|
|
|
rawURL += "?" + clonedReq.URL.RawQuery
|
|
|
|
}
|
|
|
|
urlPath, err := url.Parse(rawURL)
|
|
|
|
if err != nil {
|
|
|
|
handleCallResourceError(err, reqCtx)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
clonedReq.URL = urlPath
|
|
|
|
err = m.callResourceInternal(reqCtx.Resp, clonedReq, pCtx)
|
|
|
|
if err != nil {
|
|
|
|
handleCallResourceError(err, reqCtx)
|
|
|
|
}
|
2021-03-18 07:53:01 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) callResourceInternal(w http.ResponseWriter, req *http.Request, pCtx backend.PluginContext) error {
|
|
|
|
p := m.Plugin(pCtx.PluginID)
|
|
|
|
if p == nil {
|
|
|
|
return backendplugin.ErrPluginNotRegistered
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
keepCookieModel := keepCookiesJSONModel{}
|
|
|
|
if dis := pCtx.DataSourceInstanceSettings; dis != nil {
|
|
|
|
err := json.Unmarshal(dis.JSONData, &keepCookieModel)
|
|
|
|
if err != nil {
|
|
|
|
p.Logger().Error("Failed to to unpack JSONData in datasource instance settings", "err", err)
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
proxyutil.ClearCookieHeader(req, keepCookieModel.KeepCookies)
|
|
|
|
proxyutil.PrepareProxyRequest(req)
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
body, err := ioutil.ReadAll(req.Body)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to read request body: %w", err)
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
crReq := &backend.CallResourceRequest{
|
|
|
|
PluginContext: pCtx,
|
|
|
|
Path: req.URL.Path,
|
|
|
|
Method: req.Method,
|
|
|
|
URL: req.URL.String(),
|
|
|
|
Headers: req.Header,
|
|
|
|
Body: body,
|
|
|
|
}
|
2021-03-17 10:06:10 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return instrumentation.InstrumentCallResourceRequest(p.PluginID(), func() error {
|
|
|
|
childCtx, cancel := context.WithCancel(req.Context())
|
|
|
|
defer cancel()
|
|
|
|
stream := newCallResourceResponseStream(childCtx)
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
var wg sync.WaitGroup
|
|
|
|
wg.Add(1)
|
2021-03-18 07:53:01 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
defer func() {
|
|
|
|
if err := stream.Close(); err != nil {
|
|
|
|
m.log.Warn("Failed to close stream", "err", err)
|
|
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
}()
|
2021-03-17 10:06:10 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
var flushStreamErr error
|
|
|
|
go func() {
|
|
|
|
flushStreamErr = flushStream(p, stream, w)
|
|
|
|
wg.Done()
|
|
|
|
}()
|
2021-03-17 10:06:10 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if err := p.CallResource(req.Context(), crReq, stream); err != nil {
|
|
|
|
return err
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return flushStreamErr
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleCallResourceError(err error, reqCtx *models.ReqContext) {
|
|
|
|
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
|
|
|
|
reqCtx.JsonApiErr(503, "Plugin unavailable", err)
|
|
|
|
return
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
|
|
|
|
reqCtx.JsonApiErr(404, "Not found", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
reqCtx.JsonApiErr(500, "Failed to call resource", err)
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func flushStream(plugin backendplugin.Plugin, stream callResourceClientResponseStream, w http.ResponseWriter) error {
|
|
|
|
processedStreams := 0
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
for {
|
|
|
|
resp, err := stream.Recv()
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
if processedStreams == 0 {
|
|
|
|
return errors.New("received empty resource response")
|
|
|
|
}
|
2021-03-08 00:02:49 -06:00
|
|
|
return nil
|
|
|
|
}
|
2021-11-01 04:53:33 -05:00
|
|
|
if err != nil {
|
|
|
|
if processedStreams == 0 {
|
|
|
|
return errutil.Wrap("failed to receive response from resource call", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
plugin.Logger().Error("Failed to receive response from resource call", "err", err)
|
|
|
|
return stream.Close()
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// Expected that headers and status are only part of first stream
|
|
|
|
if processedStreams == 0 && resp.Headers != nil {
|
|
|
|
// Make sure a content type always is returned in response
|
|
|
|
if _, exists := resp.Headers["Content-Type"]; !exists {
|
|
|
|
resp.Headers["Content-Type"] = []string{"application/json"}
|
|
|
|
}
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
for k, values := range resp.Headers {
|
|
|
|
// Due to security reasons we don't want to forward
|
|
|
|
// cookies from a backend plugin to clients/browsers.
|
|
|
|
if k == "Set-Cookie" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, v := range values {
|
|
|
|
// TODO: Figure out if we should use Set here instead
|
|
|
|
// nolint:gocritic
|
|
|
|
w.Header().Add(k, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
w.WriteHeader(resp.Status)
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if _, err := w.Write(resp.Body); err != nil {
|
|
|
|
plugin.Logger().Error("Failed to write resource response", "err", err)
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
2021-11-01 04:53:33 -05:00
|
|
|
|
|
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
|
|
flusher.Flush()
|
|
|
|
}
|
|
|
|
processedStreams++
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
2021-11-01 04:53:33 -05:00
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error) {
|
|
|
|
p := m.Plugin(pluginID)
|
|
|
|
if p == nil {
|
|
|
|
return nil, backendplugin.ErrPluginNotRegistered
|
2021-03-18 07:53:01 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
var resp *backend.CollectMetricsResult
|
|
|
|
err := instrumentation.InstrumentCollectMetrics(p.PluginID(), func() (innerErr error) {
|
|
|
|
resp, innerErr = p.CollectMetrics(ctx)
|
|
|
|
return
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *PluginManager) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
|
|
|
var dsURL string
|
|
|
|
if req.PluginContext.DataSourceInstanceSettings != nil {
|
|
|
|
dsURL = req.PluginContext.DataSourceInstanceSettings.URL
|
|
|
|
}
|
|
|
|
|
|
|
|
err := m.requestValidator.Validate(dsURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
return &backend.CheckHealthResult{
|
|
|
|
Status: http.StatusForbidden,
|
|
|
|
Message: "Access denied",
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
p := m.Plugin(req.PluginContext.PluginID)
|
|
|
|
if p == nil {
|
|
|
|
return nil, backendplugin.ErrPluginNotRegistered
|
|
|
|
}
|
|
|
|
|
|
|
|
var resp *backend.CheckHealthResult
|
|
|
|
err = instrumentation.InstrumentCheckHealthRequest(p.PluginID(), func() (innerErr error) {
|
|
|
|
resp, innerErr = p.CheckHealth(ctx, &backend.CheckHealthRequest{PluginContext: req.PluginContext})
|
|
|
|
return
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
|
|
|
|
return nil, err
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
|
|
|
|
return nil, err
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return nil, errutil.Wrap("failed to check plugin health", backendplugin.ErrHealthCheckFailed)
|
|
|
|
}
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return resp, nil
|
|
|
|
}
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-15 07:25:13 -06:00
|
|
|
func (m *PluginManager) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
|
|
|
|
plugin := m.Plugin(req.PluginContext.PluginID)
|
|
|
|
if plugin == nil {
|
|
|
|
return nil, backendplugin.ErrPluginNotRegistered
|
|
|
|
}
|
|
|
|
|
|
|
|
return plugin.SubscribeStream(ctx, req)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *PluginManager) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
|
|
|
|
plugin := m.Plugin(req.PluginContext.PluginID)
|
|
|
|
if plugin == nil {
|
|
|
|
return nil, backendplugin.ErrPluginNotRegistered
|
|
|
|
}
|
|
|
|
|
|
|
|
return plugin.PublishStream(ctx, req)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *PluginManager) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
|
|
|
|
plugin := m.Plugin(req.PluginContext.PluginID)
|
|
|
|
if plugin == nil {
|
|
|
|
return backendplugin.ErrPluginNotRegistered
|
|
|
|
}
|
|
|
|
|
|
|
|
return plugin.RunStream(ctx, req, sender)
|
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) isRegistered(pluginID string) bool {
|
|
|
|
p := m.Plugin(pluginID)
|
|
|
|
if p == nil {
|
|
|
|
return false
|
|
|
|
}
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return !p.IsDecommissioned()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *PluginManager) Add(ctx context.Context, pluginID, version string, opts plugins.AddOpts) error {
|
|
|
|
var pluginZipURL string
|
|
|
|
|
|
|
|
if opts.PluginRepoURL == "" {
|
|
|
|
opts.PluginRepoURL = grafanaComURL
|
|
|
|
}
|
|
|
|
|
|
|
|
plugin := m.Plugin(pluginID)
|
|
|
|
if plugin != nil {
|
|
|
|
if !plugin.IsExternalPlugin() {
|
|
|
|
return plugins.ErrInstallCorePlugin
|
|
|
|
}
|
|
|
|
|
|
|
|
if plugin.Info.Version == version {
|
|
|
|
return plugins.DuplicateError{
|
|
|
|
PluginID: plugin.ID,
|
|
|
|
ExistingPluginDir: plugin.PluginDir,
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// get plugin update information to confirm if upgrading is possible
|
|
|
|
updateInfo, err := m.pluginInstaller.GetUpdateInfo(ctx, pluginID, version, opts.PluginRepoURL)
|
2021-03-08 00:02:49 -06:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
pluginZipURL = updateInfo.PluginZipURL
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// remove existing installation of plugin
|
|
|
|
err = m.Remove(ctx, plugin.ID)
|
|
|
|
if err != nil {
|
2021-03-08 00:02:49 -06:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if opts.PluginInstallDir == "" {
|
|
|
|
opts.PluginInstallDir = m.cfg.PluginsPath
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if opts.PluginZipURL == "" {
|
|
|
|
opts.PluginZipURL = pluginZipURL
|
|
|
|
}
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
err := m.pluginInstaller.Install(ctx, pluginID, version, opts.PluginInstallDir, opts.PluginZipURL, opts.PluginRepoURL)
|
2021-03-08 00:02:49 -06:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
err = m.loadPlugins(opts.PluginInstallDir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *PluginManager) Remove(ctx context.Context, pluginID string) error {
|
|
|
|
plugin := m.Plugin(pluginID)
|
|
|
|
if plugin == nil {
|
|
|
|
return plugins.ErrPluginNotInstalled
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if !plugin.IsExternalPlugin() {
|
|
|
|
return plugins.ErrUninstallCorePlugin
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// extra security check to ensure we only remove plugins that are located in the configured plugins directory
|
|
|
|
path, err := filepath.Rel(m.cfg.PluginsPath, plugin.PluginDir)
|
|
|
|
if err != nil || strings.HasPrefix(path, ".."+string(filepath.Separator)) {
|
|
|
|
return plugins.ErrUninstallOutsideOfPluginDir
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if m.isRegistered(pluginID) {
|
|
|
|
err := m.unregisterAndStop(ctx, plugin)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return m.pluginInstaller.Uninstall(ctx, plugin.PluginDir)
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) LoadAndRegister(pluginID string, factory backendplugin.PluginFactoryFunc) error {
|
|
|
|
if m.isRegistered(pluginID) {
|
|
|
|
return fmt.Errorf("backend plugin %s already registered", pluginID)
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
pluginRootDir := pluginID
|
|
|
|
if pluginID == "stackdriver" {
|
|
|
|
pluginRootDir = "cloud-monitoring"
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
path := filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource", pluginRootDir)
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
p, err := m.pluginLoader.LoadWithFactory(path, factory)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
err = m.register(p)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) Routes() []*plugins.StaticRoute {
|
|
|
|
staticRoutes := []*plugins.StaticRoute{}
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
for _, p := range m.Plugins() {
|
|
|
|
if p.StaticRoute() != nil {
|
|
|
|
staticRoutes = append(staticRoutes, p.StaticRoute())
|
|
|
|
}
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
2021-11-01 04:53:33 -05:00
|
|
|
return staticRoutes
|
|
|
|
}
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) registerAndStart(ctx context.Context, plugin *plugins.Plugin) error {
|
|
|
|
err := m.register(plugin)
|
2021-03-08 00:02:49 -06:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if !m.isRegistered(plugin.ID) {
|
|
|
|
return fmt.Errorf("plugin %s is not registered", plugin.ID)
|
|
|
|
}
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return m.start(ctx, plugin)
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) register(p *plugins.Plugin) error {
|
|
|
|
m.pluginsMu.Lock()
|
|
|
|
defer m.pluginsMu.Unlock()
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
pluginID := p.ID
|
|
|
|
if _, exists := m.plugins[pluginID]; exists {
|
|
|
|
return fmt.Errorf("plugin %s already registered", pluginID)
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
m.plugins[pluginID] = p
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if !p.IsCorePlugin() {
|
|
|
|
m.log.Info("Plugin registered", "pluginId", pluginID)
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return nil
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (m *PluginManager) unregisterAndStop(ctx context.Context, p *plugins.Plugin) error {
|
|
|
|
m.log.Debug("Stopping plugin process", "pluginId", p.ID)
|
|
|
|
if err := p.Decommission(); err != nil {
|
|
|
|
return err
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if err := p.Stop(ctx); err != nil {
|
|
|
|
return err
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
delete(m.plugins, p.ID)
|
2021-03-08 00:02:49 -06:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
m.log.Debug("Plugin unregistered", "pluginId", p.ID)
|
|
|
|
return nil
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// start starts a backend plugin process
|
|
|
|
func (m *PluginManager) start(ctx context.Context, p *plugins.Plugin) error {
|
|
|
|
if !p.IsManaged() || !p.Backend || p.SignatureError != nil {
|
|
|
|
return nil
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if !m.isRegistered(p.ID) {
|
|
|
|
return backendplugin.ErrPluginNotRegistered
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if err := startPluginAndRestartKilledProcesses(ctx, p); err != nil {
|
|
|
|
return err
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if !p.IsCorePlugin() {
|
|
|
|
p.Logger().Debug("Successfully started backend plugin process")
|
2021-03-08 00:02:49 -06:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return nil
|
2021-03-18 07:53:01 -05:00
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func startPluginAndRestartKilledProcesses(ctx context.Context, p *plugins.Plugin) error {
|
|
|
|
if err := p.Start(ctx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-07-29 04:52:23 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
go func(ctx context.Context, p *plugins.Plugin) {
|
|
|
|
if err := restartKilledProcess(ctx, p); err != nil {
|
|
|
|
p.Logger().Error("Attempt to restart killed plugin process failed", "error", err)
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
2021-11-01 04:53:33 -05:00
|
|
|
}(ctx, p)
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return nil
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func restartKilledProcess(ctx context.Context, p *plugins.Plugin) error {
|
|
|
|
ticker := time.NewTicker(time.Second * 1)
|
2021-07-29 04:52:23 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
case <-ticker.C:
|
|
|
|
if p.IsDecommissioned() {
|
|
|
|
p.Logger().Debug("Plugin decommissioned")
|
|
|
|
return nil
|
|
|
|
}
|
2021-07-29 04:52:23 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
if !p.Exited() {
|
|
|
|
continue
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
p.Logger().Debug("Restarting plugin")
|
|
|
|
if err := p.Start(ctx); err != nil {
|
|
|
|
p.Logger().Error("Failed to restart plugin", "error", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
p.Logger().Debug("Plugin restarted")
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
2021-11-01 04:53:33 -05:00
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// stop stops a backend plugin process
|
|
|
|
func (m *PluginManager) stop(ctx context.Context) {
|
|
|
|
m.pluginsMu.RLock()
|
|
|
|
defer m.pluginsMu.RUnlock()
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for _, p := range m.plugins {
|
|
|
|
wg.Add(1)
|
|
|
|
go func(p backendplugin.Plugin, ctx context.Context) {
|
|
|
|
defer wg.Done()
|
|
|
|
p.Logger().Debug("Stopping plugin")
|
|
|
|
if err := p.Stop(ctx); err != nil {
|
|
|
|
p.Logger().Error("Failed to stop plugin", "error", err)
|
|
|
|
}
|
|
|
|
p.Logger().Debug("Plugin stopped")
|
|
|
|
}(p, ctx)
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
2021-11-01 04:53:33 -05:00
|
|
|
wg.Wait()
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// corePluginPaths provides a list of the Core plugin paths which need to be scanned on init()
|
|
|
|
func (m *PluginManager) corePluginPaths() []string {
|
|
|
|
datasourcePaths := []string{
|
|
|
|
filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/alertmanager"),
|
|
|
|
filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/dashboard"),
|
|
|
|
filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/jaeger"),
|
|
|
|
filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/mixed"),
|
|
|
|
filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/zipkin"),
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
panelsPath := filepath.Join(m.cfg.StaticRootPath, "app/plugins/panel")
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return append(datasourcePaths, panelsPath)
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// pluginSettingPaths provides a plugin paths defined in cfg.PluginSettings which need to be scanned on init()
|
|
|
|
func (m *PluginManager) pluginSettingPaths() []string {
|
|
|
|
var pluginSettingDirs []string
|
|
|
|
for _, settings := range m.cfg.PluginSettings {
|
|
|
|
path, exists := settings["path"]
|
|
|
|
if !exists || path == "" {
|
|
|
|
continue
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
2021-11-01 04:53:33 -05:00
|
|
|
pluginSettingDirs = append(pluginSettingDirs, path)
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
return pluginSettingDirs
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
// callResourceClientResponseStream is used for receiving resource call responses.
|
|
|
|
type callResourceClientResponseStream interface {
|
|
|
|
Recv() (*backend.CallResourceResponse, error)
|
|
|
|
Close() error
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
type keepCookiesJSONModel struct {
|
|
|
|
KeepCookies []string `json:"keepCookies"`
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
type callResourceResponseStream struct {
|
|
|
|
ctx context.Context
|
|
|
|
stream chan *backend.CallResourceResponse
|
|
|
|
closed bool
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func newCallResourceResponseStream(ctx context.Context) *callResourceResponseStream {
|
|
|
|
return &callResourceResponseStream{
|
|
|
|
ctx: ctx,
|
|
|
|
stream: make(chan *backend.CallResourceResponse),
|
|
|
|
}
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (s *callResourceResponseStream) Send(res *backend.CallResourceResponse) error {
|
|
|
|
if s.closed {
|
|
|
|
return errors.New("cannot send to a closed stream")
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
select {
|
|
|
|
case <-s.ctx.Done():
|
|
|
|
return errors.New("cancelled")
|
|
|
|
case s.stream <- res:
|
|
|
|
return nil
|
|
|
|
}
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
|
|
|
|
2021-11-01 04:53:33 -05:00
|
|
|
func (s *callResourceResponseStream) Recv() (*backend.CallResourceResponse, error) {
|
|
|
|
select {
|
|
|
|
case <-s.ctx.Done():
|
|
|
|
return nil, s.ctx.Err()
|
|
|
|
case res, ok := <-s.stream:
|
|
|
|
if !ok {
|
|
|
|
return nil, io.EOF
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
2021-11-01 04:53:33 -05:00
|
|
|
return res, nil
|
2021-05-12 13:05:16 -05:00
|
|
|
}
|
|
|
|
}
|
2021-11-01 04:53:33 -05:00
|
|
|
|
|
|
|
func (s *callResourceResponseStream) Close() error {
|
|
|
|
if s.closed {
|
|
|
|
return errors.New("cannot close a closed stream")
|
|
|
|
}
|
|
|
|
|
|
|
|
close(s.stream)
|
|
|
|
s.closed = true
|
|
|
|
return nil
|
|
|
|
}
|