From 871ad7341412a5537b37eaca9dcdfb021ef430ae Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 21 Apr 2020 16:16:41 +0200 Subject: [PATCH] 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 --- Makefile | 12 +- conf/defaults.ini | 60 ++++ conf/sample.ini | 60 ++++ go.mod | 1 + pkg/api/datasources.go | 3 +- pkg/api/plugins.go | 17 +- pkg/api/render.go | 34 +- pkg/plugins/backendplugin/backend_plugin.go | 19 +- pkg/plugins/backendplugin/client.go | 26 +- .../pluginextensionv2/generate.sh | 16 + .../pluginextensionv2/renderer_grpc_plugin.go | 35 ++ .../pluginextensionv2/rendererv2.pb.go | 333 ++++++++++++++++++ .../pluginextensionv2/rendererv2.proto | 29 ++ pkg/plugins/renderer_plugin.go | 12 +- pkg/services/rendering/http_mode.go | 5 + pkg/services/rendering/interface.go | 22 +- pkg/services/rendering/plugin_mode.go | 58 ++- pkg/services/rendering/rendering.go | 37 +- scripts/protobuf-check.sh | 25 ++ vendor/modules.txt | 1 + 20 files changed, 743 insertions(+), 62 deletions(-) create mode 100755 pkg/plugins/backendplugin/pluginextensionv2/generate.sh create mode 100644 pkg/plugins/backendplugin/pluginextensionv2/renderer_grpc_plugin.go create mode 100644 pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go create mode 100644 pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto create mode 100755 scripts/protobuf-check.sh diff --git a/Makefile b/Makefile index d0e9b512783..80a78a257a0 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ -include local/Makefile -.PHONY: all deps-go deps-js deps build-go build-server build-cli build-js build build-docker-dev build-docker-full lint-go gosec revive golangci-lint go-vet test-go test-js test run run-frontend clean devenv devenv-down revive-alerting help +.PHONY: all deps-go deps-js deps build-go build-server build-cli build-js build build-docker-dev build-docker-full lint-go gosec revive golangci-lint go-vet test-go test-js test run run-frontend clean devenv devenv-down revive-alerting protobuf help GO = GO111MODULE=on go GO_FILES ?= ./pkg/... @@ -161,6 +161,16 @@ devenv-down: ## Stop optional services. ##@ Helpers +# We separate the protobuf generation because most development tasks on +# Grafana do not involve changing protobuf files and protoc is not a +# go-gettable dependency and so getting it installed can be inconvenient. +# +# If you are working on changes to protobuf interfaces you may either use +# this target or run the individual scripts below directly. +protobuf: ## Compile protobuf definitions + bash scripts/protobuf-check.sh + bash pkg/plugins/backendplugin/pluginextensionv2/generate.sh + clean: ## Clean up intermediate build artifacts. @echo "cleaning" rm -rf node_modules diff --git a/conf/defaults.ini b/conf/defaults.ini index 287be3308fc..37250cb8cde 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -693,6 +693,66 @@ disable_sanitize_html = false enable_alpha = false app_tls_skip_verify_insecure = false +#################################### Grafana Image Renderer Plugin ########################## +[plugin.grafana-image-renderer] +# Instruct headless browser instance to use a default timezone when not provided by Grafana, e.g. when rendering panel image of alert. +# See ICU’s metaZones.txt (https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt) for a list of supported +# timezone IDs. Fallbacks to TZ environment variable if not set. +rendering_timezone = + +# Instruct headless browser instance to use a default language when not provided by Grafana, e.g. when rendering panel image of alert. +# Please refer to the HTTP header Accept-Language to understand how to format this value, e.g. 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'. +rendering_language = + +# Instruct headless browser instance to use a default device scale factor when not provided by Grafana, e.g. when rendering panel image of alert. +# Default is 1. Using a higher value will produce more detailed images (higher DPI), but will require more disk space to store an image. +rendering_viewport_device_scale_factor = + +# Instruct headless browser instance whether to ignore HTTPS errors during navigation. Per default HTTPS errors are not ignored. Due to +# the security risk it's not recommended to ignore HTTPS errors. +rendering_ignore_https_errors = + +# Instruct headless browser instance whether to capture and log verbose information when rendering an image. Default is false and will +# only capture and log error messages. When enabled, debug messages are captured and logged as well. +# For the verbose information to be included in the Grafana server log you have to adjust the rendering log level to debug, configure +# [log].filter = rendering:debug. +rendering_verbose_logging = + +# Instruct headless browser instance whether to output its debug and error messages into running process of remote rendering service. +# Default is false. This can be useful to enable (true) when troubleshooting. +rendering_dumpio = + +# Additional arguments to pass to the headless browser instance. Default is --no-sandbox. The list of Chromium flags can be found +# here (https://peter.sh/experiments/chromium-command-line-switches/). Multiple arguments is separated with comma-character. +rendering_args = + +# You can configure the plugin to use a different browser binary instead of the pre-packaged version of Chromium. +# Please note that this is not recommended, since you may encounter problems if the installed version of Chrome/Chromium is not +# compatible with the plugin. +rendering_chrome_bin = + +# Instruct how headless browser instances are created. Default is 'default' and will create a new browser instance on each request. +# Mode 'clustered' will make sure that only a maximum of browsers/incognito pages can execute concurrently. +# Mode 'reusable' will have one browser instance and will create a new incognito page on each request. +rendering_mode = + +# When rendering_mode = clustered you can instruct how many browsers or incognito pages can execute concurrently. Default is 'browser' +# and will cluster using browser instances. +# Mode 'context' will cluster using incognito pages. +rendering_clustering_mode = +# When rendering_mode = clustered you can define maximum number of browser instances/incognito pages that can execute concurrently.. +rendering_clustering_max_concurrency = + +# Limit the maxiumum viewport width, height and device scale factor that can be requested. +rendering_viewport_max_width = +rendering_viewport_max_height = +rendering_viewport_max_device_scale_factor = + +# Change the listening host and port of the gRPC server. Default host is 127.0.0.1 and default port is 0 and will automatically assign +# a port not in use. +grpc_host = +grpc_port = + [enterprise] license_path = diff --git a/conf/sample.ini b/conf/sample.ini index f1dfbf2f083..8d1d007d7d6 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -682,6 +682,66 @@ ;enable_alpha = false ;app_tls_skip_verify_insecure = false +#################################### Grafana Image Renderer Plugin ########################## +[plugin.grafana-image-renderer] +# Instruct headless browser instance to use a default timezone when not provided by Grafana, e.g. when rendering panel image of alert. +# See ICU’s metaZones.txt (https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt) for a list of supported +# timezone IDs. Fallbacks to TZ environment variable if not set. +;rendering_timezone = + +# Instruct headless browser instance to use a default language when not provided by Grafana, e.g. when rendering panel image of alert. +# Please refer to the HTTP header Accept-Language to understand how to format this value, e.g. 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'. +;rendering_language = + +# Instruct headless browser instance to use a default device scale factor when not provided by Grafana, e.g. when rendering panel image of alert. +# Default is 1. Using a higher value will produce more detailed images (higher DPI), but will require more disk space to store an image. +;rendering_viewport_device_scale_factor = + +# Instruct headless browser instance whether to ignore HTTPS errors during navigation. Per default HTTPS errors are not ignored. Due to +# the security risk it's not recommended to ignore HTTPS errors. +;rendering_ignore_https_errors = + +# Instruct headless browser instance whether to capture and log verbose information when rendering an image. Default is false and will +# only capture and log error messages. When enabled, debug messages are captured and logged as well. +# For the verbose information to be included in the Grafana server log you have to adjust the rendering log level to debug, configure +# [log].filter = rendering:debug. +;rendering_verbose_logging = + +# Instruct headless browser instance whether to output its debug and error messages into running process of remote rendering service. +# Default is false. This can be useful to enable (true) when troubleshooting. +;rendering_dumpio = + +# Additional arguments to pass to the headless browser instance. Default is --no-sandbox. The list of Chromium flags can be found +# here (https://peter.sh/experiments/chromium-command-line-switches/). Multiple arguments is separated with comma-character. +;rendering_args = + +# You can configure the plugin to use a different browser binary instead of the pre-packaged version of Chromium. +# Please note that this is not recommended, since you may encounter problems if the installed version of Chrome/Chromium is not +# compatible with the plugin. +;rendering_chrome_bin = + +# Instruct how headless browser instances are created. Default is 'default' and will create a new browser instance on each request. +# Mode 'clustered' will make sure that only a maximum of browsers/incognito pages can execute concurrently. +# Mode 'reusable' will have one browser instance and will create a new incognito page on each request. +;rendering_mode = + +# When rendering_mode = clustered you can instruct how many browsers or incognito pages can execute concurrently. Default is 'browser' +# and will cluster using browser instances. +# Mode 'context' will cluster using incognito pages. +;rendering_clustering_mode = +# When rendering_mode = clustered you can define maximum number of browser instances/incognito pages that can execute concurrently.. +;rendering_clustering_max_concurrency = + +# Limit the maxiumum viewport width, height and device scale factor that can be requested. +;rendering_viewport_max_width = +;rendering_viewport_max_height = +;rendering_viewport_max_device_scale_factor = + +# Change the listening host and port of the gRPC server. Default host is 127.0.0.1 and default port is 0 and will automatically assign +# a port not in use. +;grpc_host = +;grpc_port = + [enterprise] # Path to a valid Grafana Enterprise license.jwt file ;license_path = diff --git a/go.mod b/go.mod index fb9503cc412..fbaadbf0137 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/go-sql-driver/mysql v1.4.1 github.com/go-stack/stack v1.8.0 github.com/gobwas/glob v0.2.3 + github.com/golang/protobuf v1.3.4 github.com/google/go-cmp v0.3.1 github.com/gorilla/websocket v1.4.1 github.com/gosimple/slug v1.4.2 diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index ebf86070e5e..c56562219c3 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -386,15 +386,14 @@ func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) { return } - var jsonDetails map[string]interface{} payload := map[string]interface{}{ "status": resp.Status.String(), "message": resp.Message, - "details": jsonDetails, } // 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) diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 450cd06a7a2..28c31479bd7 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "errors" "net/http" "sort" @@ -317,9 +318,19 @@ func (hs *HTTPServer) CheckHealth(c *models.ReqContext) Response { } payload := map[string]interface{}{ - "status": resp.Status.String(), - "message": resp.Message, - "jsonDetails": resp.JSONDetails, + "status": resp.Status.String(), + "message": resp.Message, + } + + // Unmarshal JSONDetails if it's not empty. + if len(resp.JSONDetails) > 0 { + var jsonDetails map[string]interface{} + err = json.Unmarshal(resp.JSONDetails, &jsonDetails) + if err != nil { + return Error(500, "Failed to unmarshal detailed response from backend plugin", err) + } + + payload["details"] = jsonDetails } if resp.Status != backendplugin.HealthStatusOk { diff --git a/pkg/api/render.go b/pkg/api/render.go index 375f905a561..bc1a20b076e 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -40,18 +40,32 @@ func (hs *HTTPServer) RenderToPng(c *models.ReqContext) { return } + scale, err := strconv.ParseFloat(queryReader.Get("scale", "1"), 64) + if err != nil { + c.Handle(400, "Render parameters error", fmt.Errorf("Cannot parse scale as float: %s", err)) + return + } + + headers := http.Header{} + acceptLanguageHeader := c.Req.Header.Values("Accept-Language") + if len(acceptLanguageHeader) > 0 { + headers["Accept-Language"] = acceptLanguageHeader + } + maxConcurrentLimitForApiCalls := 30 result, err := hs.RenderService.Render(c.Req.Context(), rendering.Opts{ - Width: width, - Height: height, - Timeout: time.Duration(timeout) * time.Second, - OrgId: c.OrgId, - UserId: c.UserId, - OrgRole: c.OrgRole, - Path: c.Params("*") + queryParams, - Timezone: queryReader.Get("tz", ""), - Encoding: queryReader.Get("encoding", ""), - ConcurrentLimit: maxConcurrentLimitForApiCalls, + Width: width, + Height: height, + Timeout: time.Duration(timeout) * time.Second, + OrgId: c.OrgId, + UserId: c.UserId, + OrgRole: c.OrgRole, + Path: c.Params("*") + queryParams, + Timezone: queryReader.Get("tz", ""), + Encoding: queryReader.Get("encoding", ""), + ConcurrentLimit: maxConcurrentLimitForApiCalls, + DeviceScaleFactor: scale, + Headers: headers, }) if err != nil && err == rendering.ErrTimeout { diff --git a/pkg/plugins/backendplugin/backend_plugin.go b/pkg/plugins/backendplugin/backend_plugin.go index bbe2ccb8c14..fc99d949a2a 100644 --- a/pkg/plugins/backendplugin/backend_plugin.go +++ b/pkg/plugins/backendplugin/backend_plugin.go @@ -6,15 +6,15 @@ import ( "net/http" "time" - "github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - datasourceV1 "github.com/grafana/grafana-plugin-model/go/datasource" rendererV1 "github.com/grafana/grafana-plugin-model/go/renderer" + "github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" "github.com/grafana/grafana/pkg/util/errutil" plugin "github.com/hashicorp/go-plugin" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // BackendPlugin a registered backend plugin. @@ -61,6 +61,11 @@ func (p *BackendPlugin) start(ctx context.Context) error { return err } + rawRenderer, err := rpcClient.Dispense("renderer") + if err != nil { + return err + } + if rawDiagnostics != nil { if plugin, ok := rawDiagnostics.(DiagnosticsPlugin); ok { p.diagnostics = plugin @@ -86,6 +91,12 @@ func (p *BackendPlugin) start(ctx context.Context) error { client.TransformPlugin = plugin } } + + if rawRenderer != nil { + if plugin, ok := rawRenderer.(pluginextensionv2.RendererPlugin); ok { + client.RendererPlugin = plugin + } + } } else { raw, err := rpcClient.Dispense(p.id) if err != nil { diff --git a/pkg/plugins/backendplugin/client.go b/pkg/plugins/backendplugin/client.go index 91ef941a1e1..769a5033fc0 100644 --- a/pkg/plugins/backendplugin/client.go +++ b/pkg/plugins/backendplugin/client.go @@ -3,11 +3,11 @@ package backendplugin import ( "os/exec" - "github.com/grafana/grafana-plugin-sdk-go/backend/grpcplugin" - "github.com/grafana/grafana/pkg/infra/log" - datasourceV1 "github.com/grafana/grafana-plugin-model/go/datasource" rendererV1 "github.com/grafana/grafana-plugin-model/go/renderer" + "github.com/grafana/grafana-plugin-sdk-go/backend/grpcplugin" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" goplugin "github.com/hashicorp/go-plugin" ) @@ -64,6 +64,17 @@ type PluginDescriptor struct { startFns PluginStartFuncs } +// getV2PluginSet returns list of plugins supported on v2. +func getV2PluginSet() goplugin.PluginSet { + return goplugin.PluginSet{ + "diagnostics": &grpcplugin.DiagnosticsGRPCPlugin{}, + "resource": &grpcplugin.ResourceGRPCPlugin{}, + "data": &grpcplugin.DataGRPCPlugin{}, + "transform": &grpcplugin.TransformGRPCPlugin{}, + "renderer": &pluginextensionv2.RendererGRPCPlugin{}, + } +} + // NewBackendPluginDescriptor creates a new backend plugin descriptor // used for registering a backend datasource plugin. func NewBackendPluginDescriptor(pluginID, executablePath string, startFns PluginStartFuncs) PluginDescriptor { @@ -75,12 +86,7 @@ func NewBackendPluginDescriptor(pluginID, executablePath string, startFns Plugin DefaultProtocolVersion: { pluginID: &datasourceV1.DatasourcePluginImpl{}, }, - grpcplugin.ProtocolVersion: { - "diagnostics": &grpcplugin.DiagnosticsGRPCPlugin{}, - "resource": &grpcplugin.ResourceGRPCPlugin{}, - "data": &grpcplugin.DataGRPCPlugin{}, - "transform": &grpcplugin.TransformGRPCPlugin{}, - }, + grpcplugin.ProtocolVersion: getV2PluginSet(), }, startFns: startFns, } @@ -97,6 +103,7 @@ func NewRendererPluginDescriptor(pluginID, executablePath string, startFns Plugi DefaultProtocolVersion: { pluginID: &rendererV1.RendererPluginImpl{}, }, + grpcplugin.ProtocolVersion: getV2PluginSet(), }, startFns: startFns, } @@ -129,4 +136,5 @@ type Client struct { ResourcePlugin ResourcePlugin DataPlugin DataPlugin TransformPlugin TransformPlugin + RendererPlugin pluginextensionv2.RendererPlugin } diff --git a/pkg/plugins/backendplugin/pluginextensionv2/generate.sh b/pkg/plugins/backendplugin/pluginextensionv2/generate.sh new file mode 100755 index 00000000000..dade69745c5 --- /dev/null +++ b/pkg/plugins/backendplugin/pluginextensionv2/generate.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# To compile all protobuf files in this repository, run +# "make protobuf" at the top-level. + +set -eu + +DST_DIR=../genproto/pluginv2 + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +cd "$DIR" + +protoc -I ./ rendererv2.proto --go_out=plugins=grpc:./ \ No newline at end of file diff --git a/pkg/plugins/backendplugin/pluginextensionv2/renderer_grpc_plugin.go b/pkg/plugins/backendplugin/pluginextensionv2/renderer_grpc_plugin.go new file mode 100644 index 00000000000..6aef8a1ecd0 --- /dev/null +++ b/pkg/plugins/backendplugin/pluginextensionv2/renderer_grpc_plugin.go @@ -0,0 +1,35 @@ +package pluginextensionv2 + +import ( + "context" + + "github.com/hashicorp/go-plugin" + "google.golang.org/grpc" +) + +type RendererPlugin interface { + RendererClient +} + +type RendererGRPCPlugin struct { + plugin.NetRPCUnsupportedPlugin +} + +func (p *RendererGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + return nil +} + +func (p *RendererGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + return &RendererGRPCClient{NewRendererClient(c)}, nil +} + +type RendererGRPCClient struct { + RendererClient +} + +func (m *RendererGRPCClient) Render(ctx context.Context, req *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error) { + return m.RendererClient.Render(ctx, req) +} + +var _ RendererClient = &RendererGRPCClient{} +var _ plugin.GRPCPlugin = &RendererGRPCPlugin{} diff --git a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go new file mode 100644 index 00000000000..7c90a2e0d7e --- /dev/null +++ b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go @@ -0,0 +1,333 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: rendererv2.proto + +package pluginextensionv2 + +import ( + context "context" + fmt "fmt" + proto "github.com/golang/protobuf/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type StringList struct { + Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *StringList) Reset() { *m = StringList{} } +func (m *StringList) String() string { return proto.CompactTextString(m) } +func (*StringList) ProtoMessage() {} +func (*StringList) Descriptor() ([]byte, []int) { + return fileDescriptor_412d7c60977d55a2, []int{0} +} + +func (m *StringList) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_StringList.Unmarshal(m, b) +} +func (m *StringList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_StringList.Marshal(b, m, deterministic) +} +func (m *StringList) XXX_Merge(src proto.Message) { + xxx_messageInfo_StringList.Merge(m, src) +} +func (m *StringList) XXX_Size() int { + return xxx_messageInfo_StringList.Size(m) +} +func (m *StringList) XXX_DiscardUnknown() { + xxx_messageInfo_StringList.DiscardUnknown(m) +} + +var xxx_messageInfo_StringList proto.InternalMessageInfo + +func (m *StringList) GetValues() []string { + if m != nil { + return m.Values + } + return nil +} + +type RenderRequest struct { + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Width int32 `protobuf:"varint,2,opt,name=width,proto3" json:"width,omitempty"` + Height int32 `protobuf:"varint,3,opt,name=height,proto3" json:"height,omitempty"` + DeviceScaleFactor float32 `protobuf:"fixed32,4,opt,name=deviceScaleFactor,proto3" json:"deviceScaleFactor,omitempty"` + FilePath string `protobuf:"bytes,5,opt,name=filePath,proto3" json:"filePath,omitempty"` + RenderKey string `protobuf:"bytes,6,opt,name=renderKey,proto3" json:"renderKey,omitempty"` + Domain string `protobuf:"bytes,7,opt,name=domain,proto3" json:"domain,omitempty"` + Timeout int32 `protobuf:"varint,8,opt,name=timeout,proto3" json:"timeout,omitempty"` + Timezone string `protobuf:"bytes,9,opt,name=timezone,proto3" json:"timezone,omitempty"` + Headers map[string]*StringList `protobuf:"bytes,10,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RenderRequest) Reset() { *m = RenderRequest{} } +func (m *RenderRequest) String() string { return proto.CompactTextString(m) } +func (*RenderRequest) ProtoMessage() {} +func (*RenderRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_412d7c60977d55a2, []int{1} +} + +func (m *RenderRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RenderRequest.Unmarshal(m, b) +} +func (m *RenderRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RenderRequest.Marshal(b, m, deterministic) +} +func (m *RenderRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_RenderRequest.Merge(m, src) +} +func (m *RenderRequest) XXX_Size() int { + return xxx_messageInfo_RenderRequest.Size(m) +} +func (m *RenderRequest) XXX_DiscardUnknown() { + xxx_messageInfo_RenderRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_RenderRequest proto.InternalMessageInfo + +func (m *RenderRequest) GetUrl() string { + if m != nil { + return m.Url + } + return "" +} + +func (m *RenderRequest) GetWidth() int32 { + if m != nil { + return m.Width + } + return 0 +} + +func (m *RenderRequest) GetHeight() int32 { + if m != nil { + return m.Height + } + return 0 +} + +func (m *RenderRequest) GetDeviceScaleFactor() float32 { + if m != nil { + return m.DeviceScaleFactor + } + return 0 +} + +func (m *RenderRequest) GetFilePath() string { + if m != nil { + return m.FilePath + } + return "" +} + +func (m *RenderRequest) GetRenderKey() string { + if m != nil { + return m.RenderKey + } + return "" +} + +func (m *RenderRequest) GetDomain() string { + if m != nil { + return m.Domain + } + return "" +} + +func (m *RenderRequest) GetTimeout() int32 { + if m != nil { + return m.Timeout + } + return 0 +} + +func (m *RenderRequest) GetTimezone() string { + if m != nil { + return m.Timezone + } + return "" +} + +func (m *RenderRequest) GetHeaders() map[string]*StringList { + if m != nil { + return m.Headers + } + return nil +} + +type RenderResponse struct { + Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RenderResponse) Reset() { *m = RenderResponse{} } +func (m *RenderResponse) String() string { return proto.CompactTextString(m) } +func (*RenderResponse) ProtoMessage() {} +func (*RenderResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_412d7c60977d55a2, []int{2} +} + +func (m *RenderResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RenderResponse.Unmarshal(m, b) +} +func (m *RenderResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RenderResponse.Marshal(b, m, deterministic) +} +func (m *RenderResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_RenderResponse.Merge(m, src) +} +func (m *RenderResponse) XXX_Size() int { + return xxx_messageInfo_RenderResponse.Size(m) +} +func (m *RenderResponse) XXX_DiscardUnknown() { + xxx_messageInfo_RenderResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_RenderResponse proto.InternalMessageInfo + +func (m *RenderResponse) GetError() string { + if m != nil { + return m.Error + } + return "" +} + +func init() { + proto.RegisterType((*StringList)(nil), "pluginextensionv2.StringList") + proto.RegisterType((*RenderRequest)(nil), "pluginextensionv2.RenderRequest") + proto.RegisterMapType((map[string]*StringList)(nil), "pluginextensionv2.RenderRequest.HeadersEntry") + proto.RegisterType((*RenderResponse)(nil), "pluginextensionv2.RenderResponse") +} + +func init() { + proto.RegisterFile("rendererv2.proto", fileDescriptor_412d7c60977d55a2) +} + +var fileDescriptor_412d7c60977d55a2 = []byte{ + // 380 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x92, 0x5f, 0x6f, 0xd3, 0x30, + 0x14, 0xc5, 0x95, 0x86, 0xa4, 0xcd, 0x2d, 0xa0, 0xd6, 0xfc, 0x91, 0x55, 0x81, 0x14, 0x2a, 0x84, + 0xf2, 0x00, 0x79, 0x48, 0x5f, 0x10, 0xbc, 0x21, 0xf1, 0x47, 0x02, 0x24, 0xe4, 0x3e, 0x95, 0xb7, + 0xac, 0xb9, 0x6b, 0xac, 0xa6, 0x76, 0xe7, 0x38, 0xd9, 0xb2, 0x6f, 0xb4, 0x6f, 0x39, 0xc5, 0x4e, + 0xd6, 0x4d, 0x9d, 0xb6, 0xb7, 0xfb, 0xf3, 0x3d, 0xbe, 0xc7, 0x3e, 0x36, 0x4c, 0x14, 0x8a, 0x0c, + 0x15, 0xaa, 0x3a, 0x89, 0xf7, 0x4a, 0x6a, 0x49, 0xa6, 0xfb, 0xa2, 0xda, 0x70, 0x81, 0x17, 0x1a, + 0x45, 0xc9, 0xa5, 0xa8, 0x93, 0xf9, 0x7b, 0x80, 0xa5, 0x56, 0x5c, 0x6c, 0xfe, 0xf0, 0x52, 0x93, + 0xd7, 0xe0, 0xd7, 0x69, 0x51, 0x61, 0x49, 0x9d, 0xd0, 0x8d, 0x02, 0xd6, 0xd1, 0xfc, 0xca, 0x85, + 0x67, 0xcc, 0x4c, 0x63, 0x78, 0x56, 0x61, 0xa9, 0xc9, 0x04, 0xdc, 0x4a, 0x15, 0xd4, 0x09, 0x9d, + 0x28, 0x60, 0x6d, 0x49, 0x5e, 0x82, 0x77, 0xce, 0x33, 0x9d, 0xd3, 0x41, 0xe8, 0x44, 0x1e, 0xb3, + 0xd0, 0x4e, 0xcc, 0x91, 0x6f, 0x72, 0x4d, 0x5d, 0xb3, 0xdc, 0x11, 0xf9, 0x08, 0xd3, 0x0c, 0x6b, + 0xbe, 0xc6, 0xe5, 0x3a, 0x2d, 0xf0, 0x47, 0xba, 0xd6, 0x52, 0xd1, 0x27, 0xa1, 0x13, 0x0d, 0xd8, + 0x71, 0x83, 0xcc, 0x60, 0x74, 0xca, 0x0b, 0xfc, 0x97, 0xea, 0x9c, 0x7a, 0xc6, 0xf2, 0x86, 0xc9, + 0x1b, 0x08, 0xec, 0x45, 0x7f, 0x63, 0x43, 0x7d, 0xd3, 0x3c, 0x2c, 0xb4, 0xfe, 0x99, 0xdc, 0xa5, + 0x5c, 0xd0, 0xa1, 0x69, 0x75, 0x44, 0x28, 0x0c, 0x35, 0xdf, 0xa1, 0xac, 0x34, 0x1d, 0x99, 0x83, + 0xf5, 0xd8, 0x7a, 0xb5, 0xe5, 0xa5, 0x14, 0x48, 0x03, 0xeb, 0xd5, 0x33, 0xf9, 0x09, 0xc3, 0x1c, + 0xd3, 0x0c, 0x55, 0x49, 0x21, 0x74, 0xa3, 0x71, 0xf2, 0x29, 0x3e, 0x8a, 0x34, 0xbe, 0x13, 0x54, + 0xfc, 0xcb, 0xea, 0xbf, 0x0b, 0xad, 0x1a, 0xd6, 0xef, 0x9e, 0xad, 0xe0, 0xe9, 0xed, 0x46, 0x1b, + 0xe7, 0x16, 0x9b, 0x3e, 0xce, 0x2d, 0x36, 0x64, 0x01, 0x9e, 0x09, 0xdf, 0xc4, 0x39, 0x4e, 0xde, + 0xde, 0x63, 0x74, 0x78, 0x38, 0x66, 0xb5, 0x5f, 0x06, 0x9f, 0x9d, 0xf9, 0x07, 0x78, 0xde, 0x9f, + 0xa0, 0xdc, 0x4b, 0x51, 0x62, 0xfb, 0x32, 0xa8, 0x94, 0x54, 0xdd, 0x78, 0x0b, 0xc9, 0x0a, 0x46, + 0xac, 0xfb, 0x20, 0xe4, 0x2f, 0xf8, 0xb6, 0x26, 0xe1, 0x63, 0x17, 0x9a, 0xbd, 0x7b, 0x40, 0x61, + 0x0d, 0xbf, 0xbd, 0xfa, 0xff, 0x22, 0xfe, 0x7a, 0xa4, 0x3a, 0xf1, 0xcd, 0x2f, 0x5c, 0x5c, 0x07, + 0x00, 0x00, 0xff, 0xff, 0x36, 0x87, 0xfd, 0x2d, 0x99, 0x02, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConnInterface + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion6 + +// RendererClient is the client API for Renderer service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type RendererClient interface { + Render(ctx context.Context, in *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error) +} + +type rendererClient struct { + cc grpc.ClientConnInterface +} + +func NewRendererClient(cc grpc.ClientConnInterface) RendererClient { + return &rendererClient{cc} +} + +func (c *rendererClient) Render(ctx context.Context, in *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error) { + out := new(RenderResponse) + err := c.cc.Invoke(ctx, "/pluginextensionv2.Renderer/Render", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// RendererServer is the server API for Renderer service. +type RendererServer interface { + Render(context.Context, *RenderRequest) (*RenderResponse, error) +} + +// UnimplementedRendererServer can be embedded to have forward compatible implementations. +type UnimplementedRendererServer struct { +} + +func (*UnimplementedRendererServer) Render(ctx context.Context, req *RenderRequest) (*RenderResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Render not implemented") +} + +func RegisterRendererServer(s *grpc.Server, srv RendererServer) { + s.RegisterService(&_Renderer_serviceDesc, srv) +} + +func _Renderer_Render_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RenderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RendererServer).Render(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pluginextensionv2.Renderer/Render", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RendererServer).Render(ctx, req.(*RenderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Renderer_serviceDesc = grpc.ServiceDesc{ + ServiceName: "pluginextensionv2.Renderer", + HandlerType: (*RendererServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Render", + Handler: _Renderer_Render_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "rendererv2.proto", +} diff --git a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto new file mode 100644 index 00000000000..a32fcb813a1 --- /dev/null +++ b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; +package pluginextensionv2; + +option go_package = ".;pluginextensionv2"; + +message StringList { + repeated string values = 1; +} + +message RenderRequest { + string url = 1; + int32 width = 2; + int32 height = 3; + float deviceScaleFactor = 4; + string filePath = 5; + string renderKey = 6; + string domain = 7; + int32 timeout = 8; + string timezone = 9; + map headers = 10; +} + +message RenderResponse { + string error = 1; +} + +service Renderer { + rpc Render(RenderRequest) returns (RenderResponse); +} diff --git a/pkg/plugins/renderer_plugin.go b/pkg/plugins/renderer_plugin.go index 1b0f87fe814..2cc4560aaca 100644 --- a/pkg/plugins/renderer_plugin.go +++ b/pkg/plugins/renderer_plugin.go @@ -8,6 +8,7 @@ import ( pluginModel "github.com/grafana/grafana-plugin-model/go/renderer" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins/backendplugin" + "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -15,7 +16,8 @@ type RendererPlugin struct { PluginBase Executable string `json:"executable,omitempty"` - GrpcPlugin pluginModel.RendererPlugin + GrpcPluginV1 pluginModel.RendererPlugin + GrpcPluginV2 pluginextensionv2.RendererPlugin backendPluginManager backendplugin.Manager } @@ -34,6 +36,7 @@ func (r *RendererPlugin) Load(decoder *json.Decoder, pluginDir string, backendPl fullpath := path.Join(r.PluginDir, cmd) descriptor := backendplugin.NewRendererPluginDescriptor(r.Id, fullpath, backendplugin.PluginStartFuncs{ OnLegacyStart: r.onLegacyPluginStart, + OnStart: r.onPluginStart, }) if err := backendPluginManager.Register(descriptor); err != nil { return errutil.Wrapf(err, "Failed to register backend plugin") @@ -52,6 +55,11 @@ func (r *RendererPlugin) Start(ctx context.Context) error { } func (r *RendererPlugin) onLegacyPluginStart(pluginID string, client *backendplugin.LegacyClient, logger log.Logger) error { - r.GrpcPlugin = client.RendererPlugin + r.GrpcPluginV1 = client.RendererPlugin + return nil +} + +func (r *RendererPlugin) onPluginStart(pluginID string, client *backendplugin.Client, logger log.Logger) error { + r.GrpcPluginV2 = client.RendererPlugin return nil } diff --git a/pkg/services/rendering/http_mode.go b/pkg/services/rendering/http_mode.go index a206154ff86..b81a39b1ee7 100644 --- a/pkg/services/rendering/http_mode.go +++ b/pkg/services/rendering/http_mode.go @@ -46,6 +46,7 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string, queryParams.Add("timezone", isoTimeOffsetToPosixTz(opts.Timezone)) queryParams.Add("encoding", opts.Encoding) queryParams.Add("timeout", strconv.Itoa(int(opts.Timeout.Seconds()))) + queryParams.Add("deviceScaleFactor", fmt.Sprintf("%f", opts.DeviceScaleFactor)) rendererUrl.RawQuery = queryParams.Encode() req, err := http.NewRequest("GET", rendererUrl.String(), nil) @@ -55,6 +56,10 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string, req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion)) + for k, v := range opts.Headers { + req.Header[k] = v + } + // gives service some additional time to timeout and return possible errors. reqContext, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2) defer cancel() diff --git a/pkg/services/rendering/interface.go b/pkg/services/rendering/interface.go index a2ff9a60ca4..754374cdfd6 100644 --- a/pkg/services/rendering/interface.go +++ b/pkg/services/rendering/interface.go @@ -13,16 +13,18 @@ var ErrNoRenderer = errors.New("No renderer plugin found nor is an external rend var ErrPhantomJSNotInstalled = errors.New("PhantomJS executable not found") type Opts struct { - Width int - Height int - Timeout time.Duration - OrgId int64 - UserId int64 - OrgRole models.RoleType - Path string - Encoding string - Timezone string - ConcurrentLimit int + Width int + Height int + Timeout time.Duration + OrgId int64 + UserId int64 + OrgRole models.RoleType + Path string + Encoding string + Timezone string + ConcurrentLimit int + DeviceScaleFactor float64 + Headers map[string][]string } type RenderResult struct { diff --git a/pkg/services/rendering/plugin_mode.go b/pkg/services/rendering/plugin_mode.go index 49451914885..475ca709e7f 100644 --- a/pkg/services/rendering/plugin_mode.go +++ b/pkg/services/rendering/plugin_mode.go @@ -6,6 +6,7 @@ import ( "time" pluginModel "github.com/grafana/grafana-plugin-model/go/renderer" + "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" ) func (rs *RenderingService) startPlugin(ctx context.Context) error { @@ -13,15 +14,23 @@ func (rs *RenderingService) startPlugin(ctx context.Context) error { } func (rs *RenderingService) renderViaPlugin(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) { + // gives plugin some additional time to timeout and return possible errors. + ctx, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2) + defer cancel() + + if rs.pluginInfo.GrpcPluginV2 != nil { + return rs.renderViaPluginV2(ctx, renderKey, opts) + } + + return rs.renderViaPluginV1(ctx, renderKey, opts) +} + +func (rs *RenderingService) renderViaPluginV1(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) { pngPath, err := rs.getFilePathForNewImage() if err != nil { return nil, err } - // gives plugin some additional time to timeout and return possible errors. - ctx, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2) - defer cancel() - req := &pluginModel.RenderRequest{ Url: rs.getURL(opts.Path), Width: int32(opts.Width), @@ -35,7 +44,46 @@ func (rs *RenderingService) renderViaPlugin(ctx context.Context, renderKey strin } rs.log.Debug("calling renderer plugin", "req", req) - rsp, err := rs.pluginInfo.GrpcPlugin.Render(ctx, req) + rsp, err := rs.pluginInfo.GrpcPluginV1.Render(ctx, req) + if err != nil { + return nil, err + } + if rsp.Error != "" { + return nil, fmt.Errorf("rendering failed: %v", rsp.Error) + } + + return &RenderResult{FilePath: pngPath}, nil +} + +func (rs *RenderingService) renderViaPluginV2(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) { + pngPath, err := rs.getFilePathForNewImage() + if err != nil { + return nil, err + } + + headers := map[string]*pluginextensionv2.StringList{} + + for k, values := range opts.Headers { + headers[k] = &pluginextensionv2.StringList{ + Values: values, + } + } + + req := &pluginextensionv2.RenderRequest{ + Url: rs.getURL(opts.Path), + Width: int32(opts.Width), + Height: int32(opts.Height), + DeviceScaleFactor: float32(opts.DeviceScaleFactor), + FilePath: pngPath, + Timeout: int32(opts.Timeout.Seconds()), + RenderKey: renderKey, + Timezone: isoTimeOffsetToPosixTz(opts.Timezone), + Domain: rs.domain, + Headers: headers, + } + rs.log.Debug("Calling renderer plugin", "req", req) + + rsp, err := rs.pluginInfo.GrpcPluginV2.Render(ctx, req) if err != nil { return nil, err } diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index 2ae6d462410..97380d79504 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -3,6 +3,7 @@ package rendering import ( "context" "fmt" + "math" "net/url" "os" "path/filepath" @@ -115,23 +116,27 @@ func (rs *RenderingService) Render(ctx context.Context, opts Opts) (*RenderResul }, nil } - if rs.renderAction != nil { - rs.log.Info("Rendering", "path", opts.Path) - renderKey, err := rs.generateAndStoreRenderKey(opts.OrgId, opts.UserId, opts.OrgRole) - if err != nil { - return nil, err - } - - defer rs.deleteRenderKey(renderKey) - - defer func() { - rs.inProgressCount-- - }() - - rs.inProgressCount++ - return rs.renderAction(ctx, renderKey, opts) + if rs.renderAction == nil { + return nil, fmt.Errorf("no renderer found") } - return nil, fmt.Errorf("No renderer found") + + rs.log.Info("Rendering", "path", opts.Path) + if math.IsInf(opts.DeviceScaleFactor, 0) || math.IsNaN(opts.DeviceScaleFactor) || opts.DeviceScaleFactor <= 0 { + opts.DeviceScaleFactor = 1 + } + renderKey, err := rs.generateAndStoreRenderKey(opts.OrgId, opts.UserId, opts.OrgRole) + if err != nil { + return nil, err + } + + defer rs.deleteRenderKey(renderKey) + + defer func() { + rs.inProgressCount-- + }() + + rs.inProgressCount++ + return rs.renderAction(ctx, renderKey, opts) } func (rs *RenderingService) GetRenderUser(key string) (*RenderUser, bool) { diff --git a/scripts/protobuf-check.sh b/scripts/protobuf-check.sh new file mode 100755 index 00000000000..77977f69646 --- /dev/null +++ b/scripts/protobuf-check.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# Check whether protobuf & go plugin are installed +PROTOC_HELP_URL="http://google.github.io/proto-lens/installing-protoc.html" +PROTOC_GEN_GO_HELP_URL="https://github.com/golang/protobuf/tree/v1.3.4#installation" + +EXIT_CODE=0 + +if ! [ -x "$(command -v protoc)" ]; then + echo "Protocol Buffers not found." + echo "Please install Protocol Buffers and ensure 'protoc' is available in your PATH." + echo "See ${PROTOC_HELP_URL} for more." + echo + EXIT_CODE=1 +fi + +if ! [ -x "$(command -v protoc-gen-go)" ]; then + echo "Protocol Buffers Go plugin not found." + echo "Please install the plugin and ensure 'protoc-gen-go' is available in your PATH." + echo "See ${PROTOC_GEN_GO_HELP_URL} for more." + echo + EXIT_CODE=1 +fi + +exit $EXIT_CODE \ No newline at end of file diff --git a/vendor/modules.txt b/vendor/modules.txt index ceba12324f2..a8384b7fc90 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -144,6 +144,7 @@ github.com/gobwas/glob/syntax/lexer github.com/gobwas/glob/util/runes github.com/gobwas/glob/util/strings # github.com/golang/protobuf v1.3.4 +## explicit github.com/golang/protobuf/proto github.com/golang/protobuf/protoc-gen-go/descriptor github.com/golang/protobuf/ptypes