How to properly set a shared golang interface for hashicorp go-plugin to expose a method that accepts json string input

47 views Asked by At

I am trying to develop a golang hashicorp go-plugin based on official published example. While the example exposes a method Greet() that does not take any argument, I am trying to expose a method CdExec(string) that accepts one argument of JSON string. The response should also respond with a JSON string with two fields 'data' and 'status'.
Unexpected behaviour: Not getting response from the plugin but no error showing. There are 3 main source codes involved.

  1. A shared interface which is imported as: shared "github.com/tcp-x/cd-plug-util". Which is published from a seperate project
  2. A source code for plugin
  3. Main file for loading the plugin
    For 2 and 3, the source is available at https://github.com/tcp-x/hashicorp-plugin-02.

Shared Interface source file:

    // package shared contains the shared interface definition
package iExec

import (
    "fmt"
    "net/rpc"

    "github.com/hashicorp/go-plugin"
)

// CdExecutor represents the interface for executing commands in a plugin.
type CdExecutor interface {
    // CdExec is a method that executes a command and returns the result.
    CdExec(jsonInput string) (string, error)
}

// Here is an implementation that talks over RPC
type CdExecutorRPCClient struct {
    client *rpc.Client
}

func (g *CdExecutorRPCClient) CdExec(req string) (string, error) {
    var resp string
    err := g.client.Call("Plugin.CdExec", new(interface{}), &resp)
    if err != nil {
        // You usually want your interfaces to return errors. If they don't,
        // there isn't much other choice here.
        panic(err)
    }

    return resp, err
}

// Here is the RPC server that CdExecutorRPC talks to, conforming to
// the requirements of net/rpc
type CdExecutorRPCServer struct {
    // This is the real implementation
    Impl CdExecutor
}


func (s *CdExecutorRPCServer) CdExec(args interface{}, resp *string) error {
    fmt.Println("CdExecutorRPCServer::args:", args)
    // req := args.(string)
    *resp, _ = s.Impl.CdExec(fmt.Sprintf("%v", args))
    return nil
}

// This is the implementation of plugin.Plugin so we can serve/consume this
//
// This has two methods: Server must return an RPC server for this plugin
// type. We construct a CdExecutorRPCServer for this.
//
// Client must return an implementation of our interface that communicates
// over an RPC client. We return CdExecutorRPC for this.
//
// Ignore MuxBroker. That is used to create more multiplexed streams on our
// plugin connection and is a more advanced use case.
type CdExecutorPlugin struct {
    // Impl Injection
    Impl CdExecutor
}

func (p *CdExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
    return &CdExecutorRPCServer{Impl: p.Impl}, nil
}

func (CdExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
    return &CdExecutorRPCClient{client: c}, nil
}

Plugin Source file:


package main

import (
    "encoding/json"
    "os"

    "github.com/hashicorp/go-hclog"
    "github.com/hashicorp/go-plugin"
    shared "github.com/tcp-x/cd-plug-util" // Import the shared package
)

type CdExecutorPlugin struct {
    logger hclog.Logger
}

// CdExec is the method that executes a command and returns the result.
func (exec *CdExecutorPlugin) CdExec(jsonInput string) (string, error) {
    exec.logger.Debug("message from CdExecutorPlugin.CdExec")
    // Parse the JSON input
    var input map[string]interface{}
    err := json.Unmarshal([]byte(jsonInput), &input)
    if err != nil {
        return "", err
    }

    // Your logic here to execute the command based on the input
    // For demonstration, let's just return a JSON response
    response := map[string]interface{}{
        "data":   "Your result data",
        "status": "success",
    }
    responseJSON, err := json.Marshal(response)
    if err != nil {
        return "", err
    }
    return string(responseJSON), nil
}

var handshakeConfig = plugin.HandshakeConfig{
    ProtocolVersion:  1,
    MagicCookieKey:   "BASIC_PLUGIN",
    MagicCookieValue: "hello",
}

func main() {
    logger := hclog.New(&hclog.LoggerOptions{
        Level:      hclog.Trace,
        Output:     os.Stderr,
        JSONFormat: true,
    })

    exec := &CdExecutorPlugin{
        logger: logger,
    }

    // pluginMap is the map of plugins we can dispense.
    var pluginMap = map[string]plugin.Plugin{
        "cd_executor": &shared.CdExecutorPlugin{Impl: exec},
    }

    logger.Debug("message from plugin", "foo", "bar")
    // Serve the plugin using the CdExecutorPlugin
    plugin.Serve(&plugin.ServeConfig{
        HandshakeConfig: handshakeConfig,
        Plugins:         pluginMap,
    })
}

Main file to load the plugin:

package main

import (
    "fmt"
    "log"
    "os"
    "os/exec"

    hclog "github.com/hashicorp/go-hclog"
    "github.com/hashicorp/go-plugin"
    shared "github.com/tcp-x/cd-plug-util" // Import the shared package
)

// handshakeConfigs are used to just do a basic handshake between
// a plugin and host. If the handshake fails, a user friendly error is shown.
// This prevents users from executing bad plugins or executing a plugin
// directory. It is a UX feature, not a security feature.
var handshakeConfig = plugin.HandshakeConfig{
    ProtocolVersion:  1,
    MagicCookieKey:   "BASIC_PLUGIN",
    MagicCookieValue: "hello",
}

// pluginMap is the map of plugins we can dispense.
var pluginMap = map[string]plugin.Plugin{
    "cd_executor": &shared.CdExecutorPlugin{},
}

func main() {
    // Create an hclog.Logger
    logger := hclog.New(&hclog.LoggerOptions{
        Name:   "plugin",
        Output: os.Stdout,
        Level:  hclog.Debug,
    })

    // Create a plugin client
    client := plugin.NewClient(&plugin.ClientConfig{
        HandshakeConfig: handshakeConfig,
        Plugins:         pluginMap,
        Cmd:             exec.Command("./plugin/executor"),
        Logger:          logger,
    })

    // Connect via RPC
    rpcClient, err := client.Client()
    if err != nil {
        log.Fatal("Error connecting:", err)
    }
    // defer rpcClient.Close()
    defer client.Kill()

    // Request the plugin
    raw, err := rpcClient.Dispense("cd_executor")
    if err != nil {
        log.Fatal("Error getting plugin:", err)
    }
    executor := raw.(shared.CdExecutor)

    // Example input JSON string
    input := `{"key": "value"}`

    // Call the CdExec method of the plugin
    result, err := executor.CdExec(input)
    if err != nil {
        log.Fatal("Error calling CdExec:", err)
    }

    fmt.Println("Plugin Result:", result)
}

Compilation script

go build -o ./plugin/executor ./plugin/plugin.go
go build -o CdPluginHost .

Executing:

./CdPluginHost

Project directory

├── bug-report-01
├── build.sh
├── CdPluginHost
├── go.mod
├── go.sum
├── main.go
└── plugin
    ├── executor
    ├── go.mod
    ├── go.sum
    └── plugin.go

Current output:

2024-03-09T02:48:17.376+0300 [DEBUG] plugin: starting plugin: path=./plugin/executor args=[./plugin/executor]
2024-03-09T02:48:17.377+0300 [DEBUG] plugin: plugin started: path=./plugin/executor pid=158963
2024-03-09T02:48:17.377+0300 [DEBUG] plugin: waiting for RPC address: plugin=./plugin/executor
2024-03-09T02:48:17.382+0300 [DEBUG] plugin.executor: message from plugin: foo=bar timestamp=2024-03-09T02:48:17.382+0300
2024-03-09T02:48:17.382+0300 [DEBUG] plugin.executor: plugin address: address=/tmp/plugin557806602 network=unix timestamp=2024-03-09T02:48:17.382+0300
2024-03-09T02:48:17.382+0300 [DEBUG] plugin: using plugin: version=1
2024-03-09T02:48:17.385+0300 [DEBUG] plugin.executor: message from CdExecutorPlugin.CdExec: timestamp=2024-03-09T02:48:17.385+0300
Plugin Result: 
2024-03-09T02:48:17.386+0300 [DEBUG] plugin.executor: 2024/03/09 02:48:17 [DEBUG] plugin: plugin server: accept unix /tmp/plugin557806602: use of closed network connection
2024-03-09T02:48:17.387+0300 [INFO]  plugin: plugin process exited: plugin=./plugin/executor id=158963
2024-03-09T02:48:17.387+0300 [DEBUG] plugin: plugin exited
0

There are 0 answers