Plugins: Add ability to run backend plugins in containers (#110534)

* add client cfg for containers

* remove unused code

* add field for skip host env for proto client

* add docker to Swagger ignore

* add to enterprise swagger gen

* undo go.mod changes

* pass container image

* propagate container image field
This commit is contained in:
Will Browne
2025-09-10 11:12:23 +01:00
committed by GitHub
parent 5c6fd5e5af
commit 0a7e0e5298
14 changed files with 150 additions and 20 deletions
+47 -5
View File
@@ -2,11 +2,15 @@ package grpcplugin
import (
"os/exec"
"runtime"
"github.com/grafana/grafana-plugin-sdk-go/backend/grpcplugin"
"github.com/hashicorp/go-hclog"
goplugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/go-plugin/runner"
"github.com/hashicorp/go-secure-stdlib/plugincontainer"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
trace "go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/embedded"
"google.golang.org/grpc"
@@ -45,7 +49,7 @@ type clientTracerProvider struct {
embedded.TracerProvider
}
func (ctp *clientTracerProvider) Tracer(instrumentationName string, opts ...trace.TracerOption) trace.Tracer {
func (ctp *clientTracerProvider) Tracer(_ string, _ ...trace.TracerOption) trace.Tracer {
return ctp.tracer
}
@@ -53,11 +57,20 @@ func newClientTracerProvider(tracer trace.Tracer) trace.TracerProvider {
return &clientTracerProvider{tracer: tracer}
}
func newClientConfig(executablePath string, args []string, env []string, skipHostEnvVars bool, logger log.Logger, tracer trace.Tracer,
versionedPlugins map[int]goplugin.PluginSet) *goplugin.ClientConfig {
func newClientConfig(descriptor PluginDescriptor, env []string, logger log.Logger, tracer trace.Tracer) *goplugin.ClientConfig {
executablePath := descriptor.executablePath
skipHostEnvVars := descriptor.skipHostEnvVars
versionedPlugins := descriptor.versionedPlugins
if runtime.GOOS == "linux" && descriptor.containerMode.enabled {
return containerClientConfig(executablePath, descriptor.containerMode.image, logger, versionedPlugins, skipHostEnvVars, tracer)
}
logger.Info("Using process mode", "os", runtime.GOOS, "executablePath", executablePath)
// We can ignore gosec G201 here, since the dynamic part of executablePath comes from the plugin definition
// nolint:gosec
cmd := exec.Command(executablePath, args...)
cmd := exec.Command(executablePath, descriptor.executableArgs...)
cmd.Env = env
return &goplugin.ClientConfig{
@@ -79,6 +92,29 @@ func newClientConfig(executablePath string, args []string, env []string, skipHos
}
}
func containerClientConfig(executablePath, containerImage string, logger log.Logger, versionedPlugins map[int]goplugin.PluginSet, skipHostEnvVars bool, tracer trace.Tracer) *goplugin.ClientConfig {
logger.Debug("Linux host detected - using container mode", "executable", executablePath)
return &goplugin.ClientConfig{
RunnerFunc: func(l hclog.Logger, cmd *exec.Cmd, tmpDir string) (runner.Runner, error) {
logger.Info("Creating container runner", "executablePath", executablePath, "tmpDir", tmpDir)
config := &plugincontainer.Config{
Image: containerImage,
Env: cmd.Env,
}
return config.NewContainerRunner(l, cmd, tmpDir)
},
HandshakeConfig: handshake,
VersionedPlugins: versionedPlugins,
SkipHostEnv: skipHostEnvVars,
Logger: logWrapper{Logger: logger},
AllowedProtocols: []goplugin.Protocol{goplugin.ProtocolGRPC},
GRPCDialOptions: []grpc.DialOption{
grpc.WithStatsHandler(otelgrpc.NewClientHandler(otelgrpc.WithTracerProvider(newClientTracerProvider(tracer)))),
},
}
}
// StartRendererFunc callback function called when a renderer plugin is started.
type StartRendererFunc func(pluginID string, renderer pluginextensionv2.RendererPlugin, logger log.Logger) error
@@ -89,10 +125,16 @@ type PluginDescriptor struct {
executableArgs []string
skipHostEnvVars bool
managed bool
containerMode containerModeOpts
versionedPlugins map[int]goplugin.PluginSet
startRendererFn StartRendererFunc
}
type containerModeOpts struct {
enabled bool
image string
}
// NewBackendPlugin creates a new backend plugin factory used for registering a backend plugin.
func NewBackendPlugin(pluginID, executablePath string, skipHostEnvVars bool, executableArgs ...string) backendplugin.PluginFactoryFunc {
return newBackendPlugin(pluginID, executablePath, true, skipHostEnvVars, executableArgs...)
@@ -4,7 +4,7 @@ import (
"context"
"errors"
trace "go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
@@ -44,12 +44,19 @@ type protoClient struct {
}
type ProtoClientOpts struct {
PluginJSON plugins.JSONData
ExecutablePath string
ExecutableArgs []string
Env []string
Logger log.Logger
Tracer trace.Tracer
PluginJSON plugins.JSONData
ExecutablePath string
ExecutableArgs []string
Env []string
ContainerMode ContainerModeOpts
SkipHostEnvVars bool
Logger log.Logger
Tracer trace.Tracer
}
type ContainerModeOpts struct {
Enabled bool
Image string
}
func NewProtoClient(opts ProtoClientOpts) (ProtoClient, error) {
@@ -60,6 +67,11 @@ func NewProtoClient(opts ProtoClientOpts) (ProtoClient, error) {
executablePath: opts.ExecutablePath,
executableArgs: opts.ExecutableArgs,
versionedPlugins: pluginSet,
containerMode: containerModeOpts{
enabled: opts.ContainerMode.Enabled,
image: opts.ContainerMode.Image,
},
skipHostEnvVars: opts.SkipHostEnvVars,
},
opts.Logger,
opts.Tracer,
@@ -7,7 +7,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/hashicorp/go-plugin"
trace "go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/infra/process"
"github.com/grafana/grafana/pkg/plugins"
@@ -48,7 +48,7 @@ func newGrpcPlugin(descriptor PluginDescriptor, logger log.Logger, tracer trace.
descriptor: descriptor,
logger: logger,
clientFactory: func() *plugin.Client {
return plugin.NewClient(newClientConfig(descriptor.executablePath, descriptor.executableArgs, env(), descriptor.skipHostEnvVars, logger, tracer, descriptor.versionedPlugins))
return plugin.NewClient(newClientConfig(descriptor, env(), logger, tracer))
},
state: pluginStateNotStarted,
}