mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-12 02:21:29 +08:00
feat: support switch hashicorp plugin with environment HRP_PLUGIN_TYPE
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user