Skip to content

Commit

Permalink
Merge pull request #4574 from hashicorp/f-base-go-plugin
Browse files Browse the repository at this point in the history
Base go-plugin client/server
  • Loading branch information
dadgar authored Aug 13, 2018
2 parents 5f03b31 + 2de5989 commit a58faf0
Show file tree
Hide file tree
Showing 40 changed files with 4,857 additions and 74 deletions.
35 changes: 35 additions & 0 deletions plugins/base/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package base

import (
"github.com/hashicorp/nomad/plugins/shared/hclspec"
)

// BasePlugin is the interface that all Nomad plugins must support.
type BasePlugin interface {
// PluginInfo describes the type and version of a plugin.
PluginInfo() (*PluginInfoResponse, error)

// ConfigSchema returns the schema for parsing the plugins configuration.
ConfigSchema() (*hclspec.Spec, error)

// SetConfig is used to set the configuration by passing a MessagePack
// encoding of it.
SetConfig(data []byte) error
}

// PluginInfoResponse returns basic information about the plugin such that Nomad
// can decide whether to load the plugin or not.
type PluginInfoResponse struct {
// Type returns the plugins type
Type string

// PluginApiVersion returns the version of the Nomad plugin API it is built
// against.
PluginApiVersion string

// PluginVersion is the version of the plugin.
PluginVersion string

// Name is the plugins name.
Name string
}
59 changes: 59 additions & 0 deletions plugins/base/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package base

import (
"context"
"fmt"

"github.com/hashicorp/nomad/plugins/base/proto"
"github.com/hashicorp/nomad/plugins/shared/hclspec"
)

// basePluginClient implements the client side of a remote base plugin, using
// gRPC to communicate to the remote plugin.
type basePluginClient struct {
client proto.BasePluginClient
}

func (b *basePluginClient) PluginInfo() (*PluginInfoResponse, error) {
presp, err := b.client.PluginInfo(context.Background(), &proto.PluginInfoRequest{})
if err != nil {
return nil, err
}

var ptype string
switch presp.GetType() {
case proto.PluginType_DRIVER:
ptype = PluginTypeDriver
case proto.PluginType_DEVICE:
ptype = PluginTypeDevice
default:
return nil, fmt.Errorf("plugin is of unknown type: %q", presp.GetType().String())
}

resp := &PluginInfoResponse{
Type: ptype,
PluginApiVersion: presp.GetPluginApiVersion(),
PluginVersion: presp.GetPluginVersion(),
Name: presp.GetName(),
}

return resp, nil
}

func (b *basePluginClient) ConfigSchema() (*hclspec.Spec, error) {
presp, err := b.client.ConfigSchema(context.Background(), &proto.ConfigSchemaRequest{})
if err != nil {
return nil, err
}

return presp.GetSpec(), nil
}

func (b *basePluginClient) SetConfig(data []byte) error {
// Send the config
_, err := b.client.SetConfig(context.Background(), &proto.SetConfigRequest{
MsgpackConfig: data,
})

return err
}
18 changes: 18 additions & 0 deletions plugins/base/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package base

import (
"github.com/hashicorp/nomad/plugins/shared/hclspec"
)

// MockPlugin is used for testing.
// Each function can be set as a closure to make assertions about how data
// is passed through the base plugin layer.
type MockPlugin struct {
PluginInfoF func() (*PluginInfoResponse, error)
ConfigSchemaF func() (*hclspec.Spec, error)
SetConfigF func([]byte) error
}

func (p *MockPlugin) PluginInfo() (*PluginInfoResponse, error) { return p.PluginInfoF() }
func (p *MockPlugin) ConfigSchema() (*hclspec.Spec, error) { return p.ConfigSchemaF() }
func (p *MockPlugin) SetConfig(data []byte) error { return p.SetConfigF(data) }
45 changes: 45 additions & 0 deletions plugins/base/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package base

import (
"context"

plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/nomad/plugins/base/proto"
"google.golang.org/grpc"
)

const (
// PluginTypeDriver implements the driver plugin interface
PluginTypeDriver = "driver"

// PluginTypeDevice implements the device plugin interface
PluginTypeDevice = "device"
)

var (
// Handshake is a common handshake that is shared by all plugins and Nomad.
Handshake = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "NOMAD_PLUGIN_MAGIC_COOKIE",
MagicCookieValue: "e4327c2e01eabfd75a8a67adb114fb34a757d57eee7728d857a8cec6e91a7255",
}
)

// PluginBase is wraps a BasePlugin and implements go-plugins GRPCPlugin
// interface to expose the interface over gRPC.
type PluginBase struct {
plugin.NetRPCUnsupportedPlugin
impl BasePlugin
}

func (p *PluginBase) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
proto.RegisterBasePluginServer(s, &basePluginServer{
impl: p.impl,
broker: broker,
})
return nil
}

func (p *PluginBase) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &basePluginClient{client: proto.NewBasePluginClient(c)}, nil
}
198 changes: 198 additions & 0 deletions plugins/base/plugin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package base

import (
"testing"

pb "github.com/golang/protobuf/proto"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/plugins/shared/hclspec"
"github.com/stretchr/testify/require"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/msgpack"
)

var (
// testSpec is an hcl Spec for testing
testSpec = &hclspec.Spec{
Block: &hclspec.Spec_Object{
Object: &hclspec.Object{
Attributes: map[string]*hclspec.Spec{
"foo": {
Block: &hclspec.Spec_Attr{
Attr: &hclspec.Attr{
Type: "string",
Required: false,
},
},
},
"bar": {
Block: &hclspec.Spec_Attr{
Attr: &hclspec.Attr{
Type: "number",
Required: true,
},
},
},
"baz": {
Block: &hclspec.Spec_Attr{
Attr: &hclspec.Attr{
Type: "bool",
},
},
},
},
},
},
}
)

// testConfig is used to decode a config from the testSpec
type testConfig struct {
Foo string `cty:"foo" codec:"foo"`
Bar int64 `cty:"bar" codec:"bar"`
Baz bool `cty:"baz" codec:"baz"`
}

func TestBasePlugin_PluginInfo_GRPC(t *testing.T) {
t.Parallel()
require := require.New(t)

const (
apiVersion = "v0.1.0"
pluginVersion = "v0.2.1"
pluginName = "mock"
)

knownType := func() (*PluginInfoResponse, error) {
info := &PluginInfoResponse{
Type: PluginTypeDriver,
PluginApiVersion: apiVersion,
PluginVersion: pluginVersion,
Name: pluginName,
}
return info, nil
}
unknownType := func() (*PluginInfoResponse, error) {
info := &PluginInfoResponse{
Type: "bad",
PluginApiVersion: apiVersion,
PluginVersion: pluginVersion,
Name: pluginName,
}
return info, nil
}

mock := &MockPlugin{
PluginInfoF: knownType,
}

client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"base": &PluginBase{impl: mock},
})
defer server.Stop()
defer client.Close()

raw, err := client.Dispense("base")
if err != nil {
t.Fatalf("err: %s", err)
}

impl, ok := raw.(BasePlugin)
if !ok {
t.Fatalf("bad: %#v", raw)
}

resp, err := impl.PluginInfo()
require.NoError(err)
require.Equal(apiVersion, resp.PluginApiVersion)
require.Equal(pluginVersion, resp.PluginVersion)
require.Equal(pluginName, resp.Name)
require.Equal(PluginTypeDriver, resp.Type)

// Swap the implementation to return an unknown type
mock.PluginInfoF = unknownType
_, err = impl.PluginInfo()
require.Error(err)
require.Contains(err.Error(), "unknown type")
}

func TestBasePlugin_ConfigSchema(t *testing.T) {
t.Parallel()
require := require.New(t)

mock := &MockPlugin{
ConfigSchemaF: func() (*hclspec.Spec, error) {
return testSpec, nil
},
}

client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"base": &PluginBase{impl: mock},
})
defer server.Stop()
defer client.Close()

raw, err := client.Dispense("base")
if err != nil {
t.Fatalf("err: %s", err)
}

impl, ok := raw.(BasePlugin)
if !ok {
t.Fatalf("bad: %#v", raw)
}

specOut, err := impl.ConfigSchema()
require.NoError(err)
require.True(pb.Equal(testSpec, specOut))
}

func TestBasePlugin_SetConfig(t *testing.T) {
t.Parallel()
require := require.New(t)

var receivedData []byte
mock := &MockPlugin{
ConfigSchemaF: func() (*hclspec.Spec, error) {
return testSpec, nil
},
SetConfigF: func(data []byte) error {
receivedData = data
return nil
},
}

client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"base": &PluginBase{impl: mock},
})
defer server.Stop()
defer client.Close()

raw, err := client.Dispense("base")
if err != nil {
t.Fatalf("err: %s", err)
}

impl, ok := raw.(BasePlugin)
if !ok {
t.Fatalf("bad: %#v", raw)
}

config := cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("v1"),
"bar": cty.NumberIntVal(1337),
"baz": cty.BoolVal(true),
})
cdata, err := msgpack.Marshal(config, config.Type())
require.NoError(err)
require.NoError(impl.SetConfig(cdata))
require.Equal(cdata, receivedData)

// Decode the value back
var actual testConfig
require.NoError(structs.Decode(receivedData, &actual))
require.Equal("v1", actual.Foo)
require.EqualValues(1337, actual.Bar)
require.True(actual.Baz)
}
Loading

0 comments on commit a58faf0

Please sign in to comment.