-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4574 from hashicorp/f-base-go-plugin
Base go-plugin client/server
- Loading branch information
Showing
40 changed files
with
4,857 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.