From ea89a0b6a98b02c4596c9856c17f9547a9008e2c Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 4 Mar 2022 20:34:24 +0800 Subject: [PATCH] feat: support switch hashicorp plugin with environment HRP_PLUGIN_TYPE --- docs/CHANGELOG.md | 2 +- internal/builtin/function.go | 4 ++-- internal/json/json.go | 1 + plugin/README.md | 12 ++++++++++++ plugin/inner/config.go | 22 +++++++++++++++++++++- plugin/inner/grpc.go | 10 +++++----- plugin/inner/hashicorp_plugin.go | 26 +++++++++++++++++++------- plugin/plugin.go | 17 ++++++++++++----- 8 files changed, 73 insertions(+), 21 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 25fdd559..8d10d6c3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,7 +2,7 @@ ## v0.7.0 (2022-03-04) -- refactor: replace hashicorp plugin from net/rpc to gPRC mode +- feat: both support gPRC(default) and net/rpc mode in hashicorp plugin, switch with environment `HRP_PLUGIN_TYPE` - refactor: replace builtin json library with json-iterator/go to improve performance ## v0.6.3 (2022-03-04) diff --git a/internal/builtin/function.go b/internal/builtin/function.go index 70cff18e..ea7a6720 100644 --- a/internal/builtin/function.go +++ b/internal/builtin/function.go @@ -96,7 +96,7 @@ func Dump2JSON(data interface{}, path string) error { return err } log.Info().Str("path", path).Msg("dump data to json") - file, _ := json.MarshalIndent(data, "", "\t") + file, _ := json.MarshalIndent(data, "", " ") err = os.WriteFile(path, file, 0644) if err != nil { log.Error().Err(err).Msg("dump json path failed") @@ -137,7 +137,7 @@ func FormatResponse(raw interface{}) interface{} { for key, value := range raw.(map[string]interface{}) { // convert value to json if key == "body" { - b, _ := json.MarshalIndent(&value, "", "\t") + b, _ := json.MarshalIndent(&value, "", " ") value = string(b) } formattedResponse[key] = value diff --git a/internal/json/json.go b/internal/json/json.go index 9a5649ae..ca3efbf5 100644 --- a/internal/json/json.go +++ b/internal/json/json.go @@ -4,6 +4,7 @@ import ( jsoniter "github.com/json-iterator/go" ) +// replace with third-party json library to improve performance var json = jsoniter.ConfigCompatibleWithStandardLibrary var ( diff --git a/plugin/README.md b/plugin/README.md index b83e2c10..91a3a601 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -99,6 +99,18 @@ Then, you can call your defined plugin function in your `YAML/JSON` testcase at } ``` +### rpc vs. gRPC + +HttpRunner+ has both supported `net/rpc` and `gRPC` in [hashicorp/plugin]. It is recommended to use `gRPC` and this is the default choice. + +If you want to run plugin in `net/rpc` mode, you can set an environment variable `HRP_PLUGIN_TYPE=rpc`. + +```bash +$ export HRP_PLUGIN_TYPE=rpc +$ hrp run examples/demo.json +$ hrp boom examples/demo.json +``` + ## go plugin The golang official plugin is only supported on Linux, FreeBSD, and macOS. And this solution also has many drawbacks. diff --git a/plugin/inner/config.go b/plugin/inner/config.go index df013eb2..791fee9a 100644 --- a/plugin/inner/config.go +++ b/plugin/inner/config.go @@ -1,6 +1,11 @@ package pluginInternal -import "github.com/hashicorp/go-plugin" +import ( + "os" + "strings" + + "github.com/hashicorp/go-plugin" +) const PluginName = "debugtalk" const RPCPluginName = PluginName + "_rpc" @@ -15,3 +20,18 @@ var HandshakeConfig = plugin.HandshakeConfig{ MagicCookieKey: "HttpRunnerPlus", MagicCookieValue: PluginName, } + +const hrpPluginTypeEnvName = "HRP_PLUGIN_TYPE" + +var hrpPluginType string + +func init() { + hrpPluginType = strings.ToLower(os.Getenv(hrpPluginTypeEnvName)) + if hrpPluginType == "" { + hrpPluginType = "grpc" // default + } +} + +func IsRPCPluginType() bool { + return hrpPluginType == "rpc" +} diff --git a/plugin/inner/grpc.go b/plugin/inner/grpc.go index 1c38a602..e2761b17 100644 --- a/plugin/inner/grpc.go +++ b/plugin/inner/grpc.go @@ -30,14 +30,14 @@ func (m *functionGRPCClient) GetNames() ([]string, error) { func (m *functionGRPCClient) Call(funcName string, funcArgs ...interface{}) (interface{}, error) { log.Info().Str("funcName", funcName).Interface("funcArgs", funcArgs).Msg("call function via gRPC") - req := &proto.CallRequest{ - Name: funcName, - } funcArgBytes, err := json.Marshal(funcArgs) if err != nil { return nil, errors.Wrap(err, "failed to marshal Call() funcArgs") } - req.Args = funcArgBytes + req := &proto.CallRequest{ + Name: funcName, + Args: funcArgBytes, + } response, err := m.client.Call(context.Background(), req) if err != nil { @@ -65,7 +65,7 @@ func (m *functionGRPCServer) GetNames(ctx context.Context, req *proto.Empty) (*p log.Info().Interface("req", req).Msg("gRPC GetNames() called on plugin side") v, err := m.Impl.GetNames() if err != nil { - log.Error().Err(err).Msg("gRPC GetNames execution failed") + log.Error().Err(err).Msg("gRPC GetNames() execution failed") return nil, err } return &proto.GetNamesResponse{Names: v}, err diff --git a/plugin/inner/hashicorp_plugin.go b/plugin/inner/hashicorp_plugin.go index 1e0996d9..7b4cbd39 100644 --- a/plugin/inner/hashicorp_plugin.go +++ b/plugin/inner/hashicorp_plugin.go @@ -1,11 +1,13 @@ package pluginInternal import ( + "fmt" "os" "os/exec" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" + "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -19,7 +21,14 @@ type HashicorpPlugin struct { } func (p *HashicorpPlugin) Init(path string) error { - pluginName := GRPCPluginName + var pluginName string + if IsRPCPluginType() { + pluginName = RPCPluginName + } else { + pluginName = GRPCPluginName + } + + // logger loggerOptions := &hclog.LoggerOptions{ Name: pluginName, Output: os.Stdout, @@ -29,6 +38,11 @@ func (p *HashicorpPlugin) Init(path string) error { } else { loggerOptions.Level = hclog.Info } + + // cmd + cmd := exec.Command(path) + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", hrpPluginTypeEnvName, hrpPluginType)) + // launch the plugin process client = plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: HandshakeConfig, @@ -36,7 +50,7 @@ func (p *HashicorpPlugin) Init(path string) error { RPCPluginName: &RPCPlugin{}, GRPCPluginName: &GRPCPlugin{}, }, - Cmd: exec.Command(path), + Cmd: cmd, Logger: hclog.New(loggerOptions), AllowedProtocols: []plugin.Protocol{ plugin.ProtocolNetRPC, @@ -44,18 +58,16 @@ func (p *HashicorpPlugin) Init(path string) error { }, }) - // Connect via RPC + // Connect via RPC/gRPC rpcClient, err := client.Client() if err != nil { - log.Error().Err(err).Msg("connect plugin via RPC failed") - return err + return errors.Wrap(err, fmt.Sprintf("connect %s plugin failed", hrpPluginType)) } // Request the plugin raw, err := rpcClient.Dispense(pluginName) if err != nil { - log.Error().Err(err).Msg("request plugin failed") - return err + return errors.Wrap(err, fmt.Sprintf("request %s plugin failed", hrpPluginType)) } // We should have a Function now! This feels like a normal interface diff --git a/plugin/plugin.go b/plugin/plugin.go index a2dc7c22..fc32774b 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -51,8 +51,8 @@ func Register(funcName string, fn interface{}) { functions[funcName] = reflect.ValueOf(fn) } -// Serve starts a plugin server process in RPC mode. -func ServeRPC() { +// serveRPC starts a plugin server process in RPC mode. +func serveRPC() { log.Info().Msg("start plugin server in RPC mode") funcPlugin := &functionPlugin{ logger: hclog.New(&hclog.LoggerOptions{ @@ -72,8 +72,8 @@ func ServeRPC() { }) } -// Serve starts a plugin server process in gRPC mode. -func ServeGRPC() { +// serveGRPC starts a plugin server process in gRPC mode. +func serveGRPC() { log.Info().Msg("start plugin server in gRPC mode") funcPlugin := &functionPlugin{ logger: hclog.New(&hclog.LoggerOptions{ @@ -95,4 +95,11 @@ func ServeGRPC() { } // default to run plugin in gRPC mode -var Serve = ServeGRPC +func Serve() { + if pluginInternal.IsRPCPluginType() { + serveRPC() + } else { + // default + serveGRPC() + } +}