Skip to content
This repository has been archived by the owner on Jan 21, 2020. It is now read-only.

Commit

Permalink
Add Version information to plugin APIs (#318)
Browse files Browse the repository at this point in the history
Signed-off-by: Bill Farner <bill@docker.com>
  • Loading branch information
wfarner authored and David Chung committed Dec 5, 2016
1 parent a1e94af commit eab86f0
Show file tree
Hide file tree
Showing 22 changed files with 294 additions and 21 deletions.
15 changes: 13 additions & 2 deletions docs/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,15 @@ _InfraKit_ plugins are exposed via HTTP, using [JSON-RPC 2.0](http://www.jsonrpc
API requests can be made manually with `curl`. For example, the following command will list all groups:
```console
$ curl -X POST --unix-socket ~/.infrakit/plugins/group http:/rpc \
-H 'Content-Type: application/json'
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"Group.InspectGroups","params":{},"id":1}'
{"jsonrpc":"2.0","result":{"Groups":null},"id":1}
```

API errors are surfaced with the `error` response field:
```console
$ curl -X POST --unix-socket ~/.infrakit/plugins/group http:/rpc \
-H 'Content-Type: application/json'
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"Group.CommitGroup","params":{},"id":1}'
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Group ID must not be blank","data":null},"id":1}
```
Expand All @@ -135,3 +135,14 @@ for each plugin type:
See also: documentation on common API [types](types.md).

Additionally, all plugins will log each API HTTP request and response when run with the `--log 5` command line argument.

##### API identification
Plugins are required to identify the name and version of plugin APIs they implement. This is done with a request
like the following:

```console
$ curl -X POST --unix-socket ~/.infrakit/plugins/group http:/rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"Plugin.Implements","params":{},"id":1}'
{"jsonrpc":"2.0","result":{"Interfaces":[{"Name":"Group","Version":"0.1.0"}]},"id":1}
```
2 changes: 1 addition & 1 deletion docs/plugins/flavor.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Flavor plugin API

<!-- SOURCE-CHECKSUM pkg/spi/flavor/* 81a2c81f42a56ce0baa54511ee621f885fc7080e -->
<!-- SOURCE-CHECKSUM pkg/spi/flavor/* 921b81c90c2abc7aec298333e1e1cf9c039afca5 -->

## API

Expand Down
2 changes: 1 addition & 1 deletion docs/plugins/group.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Group plugin API

<!-- SOURCE-CHECKSUM pkg/spi/group/* 0eec99ab5b4dc627b4025e29fb97dba4ced8c16f -->
<!-- SOURCE-CHECKSUM pkg/spi/group/* 4bc86b2ae0893db92f880ab4bb2479b5def55746 -->

## API

Expand Down
2 changes: 1 addition & 1 deletion docs/plugins/instance.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Instance plugin API

<!-- SOURCE-CHECKSUM pkg/spi/instance/* 0c778e96cbeb32043532709412e15e6cc86778d7338393f886f528c3824986fc97cb27410aefd8e2 -->
<!-- SOURCE-CHECKSUM pkg/spi/instance/* 8fc5d1832d0d96d01d8d76ea1137230790fe51fe338393f886f528c3824986fc97cb27410aefd8e2 -->

## API

Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/serverutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

// RunPlugin runs a plugin server, advertising with the provided name for discovery.
// The plugin should conform to the rpc call convention as implemented in the rpc package.
func RunPlugin(name string, plugin interface{}) {
func RunPlugin(name string, plugin server.VersionedInterface) {
stoppable, err := server.StartPluginAtPath(path.Join(discovery.Dir(), name), plugin)
if err != nil {
log.Error(err)
Expand Down
19 changes: 10 additions & 9 deletions pkg/rpc/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,35 @@ package client
import (
"bytes"
log "github.com/Sirupsen/logrus"
"github.com/docker/infrakit/pkg/spi"
"github.com/gorilla/rpc/v2/json2"
"net"
"net/http"
"net/http/httputil"
"sync"
)

// Client is an HTTP client for sending JSON-RPC requests.
type Client struct {
type client struct {
http http.Client
}

// New creates a new Client that communicates with a unix socke.
func New(socketPath string) Client {
// New creates a new Client that communicates with a unix socket and validates the remote API.
func New(socketPath string, api spi.InterfaceSpec) Client {
dialUnix := func(proto, addr string) (conn net.Conn, err error) {
return net.Dial("unix", socketPath)
}

return Client{http: http.Client{Transport: &http.Transport{Dial: dialUnix}}}
unvalidatedClient := &client{http: http.Client{Transport: &http.Transport{Dial: dialUnix}}}
return &handshakingClient{client: unvalidatedClient, iface: api, lock: &sync.Mutex{}}
}

// Call sends an RPC with a method and argument. The result must be a pointer to the response object.
func (c Client) Call(method string, arg interface{}, result interface{}) error {
func (c client) Call(method string, arg interface{}, result interface{}) error {
message, err := json2.EncodeClientRequest(method, arg)
if err != nil {
return err
}

req, err := http.NewRequest("POST", "http:///", bytes.NewReader(message))
req, err := http.NewRequest("POST", "http://a/", bytes.NewReader(message))
if err != nil {
return err
}
Expand All @@ -43,7 +44,7 @@ func (c Client) Call(method string, arg interface{}, result interface{}) error {
log.Error(err)
}

resp, err := c.http.Post("http://d/rpc", "application/json", bytes.NewReader(message))
resp, err := c.http.Do(req)
if err != nil {
return err
}
Expand Down
70 changes: 70 additions & 0 deletions pkg/rpc/client/handshake.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package client

import (
"fmt"
"github.com/docker/infrakit/pkg/rpc/plugin"
"github.com/docker/infrakit/pkg/spi"
"sync"
)

type handshakingClient struct {
client Client
iface spi.InterfaceSpec

// handshakeResult handles the tri-state outcome of handshake state:
// - handshake has not yet completed (nil)
// - handshake completed successfully (non-nil result, nil error)
// - handshake failed (non-nil result, non-nil error)
handshakeResult *handshakeResult

// lock guards handshakeResult
lock *sync.Mutex
}

type handshakeResult struct {
err error
}

func (c *handshakingClient) handshake() error {
c.lock.Lock()
defer c.lock.Unlock()

if c.handshakeResult == nil {
req := plugin.ImplementsRequest{}
resp := plugin.ImplementsResponse{}

if err := c.client.Call("Plugin.Implements", req, &resp); err != nil {
return err
}

err := fmt.Errorf("Plugin does not support interface %v", c.iface)
if resp.APIs != nil {
for _, iface := range resp.APIs {
if iface.Name == c.iface.Name {
if iface.Version == c.iface.Version {
err = nil
break
} else {
err = fmt.Errorf(
"Plugin supports %s interface version %s, client requires %s",
iface.Name,
iface.Version,
c.iface.Version)
}
}
}
}

c.handshakeResult = &handshakeResult{err: err}
}

return c.handshakeResult.err
}

func (c *handshakingClient) Call(method string, arg interface{}, result interface{}) error {
if err := c.handshake(); err != nil {
return err
}

return c.client.Call(method, arg, result)
}
85 changes: 85 additions & 0 deletions pkg/rpc/client/handshake_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package client

import (
"github.com/docker/infrakit/pkg/rpc/server"
"github.com/docker/infrakit/pkg/spi"
"github.com/stretchr/testify/require"
"io/ioutil"
"net/http"
"path/filepath"
"testing"
)

var apiSpec = spi.InterfaceSpec{
Name: "TestPlugin",
Version: "0.1.0",
}

func startPluginServer(t *testing.T) (server.Stoppable, string) {
dir, err := ioutil.TempDir("", "infrakit_handshake_test")
require.NoError(t, err)

name := "instance"
socket := filepath.Join(dir, name)

testServer, err := server.StartPluginAtPath(socket, &TestPlugin{spec: apiSpec})
require.NoError(t, err)
return testServer, socket
}

func TestHandshakeSuccess(t *testing.T) {
testServer, socket := startPluginServer(t)
defer testServer.Stop()

client := rpcClient{client: New(socket, apiSpec)}
require.NoError(t, client.DoSomething())
}

func TestHandshakeFailVersion(t *testing.T) {
testServer, socket := startPluginServer(t)
defer testServer.Stop()

client := rpcClient{client: New(socket, spi.InterfaceSpec{Name: "TestPlugin", Version: "0.2.0"})}
err := client.DoSomething()
require.Error(t, err)
require.Equal(t, "Plugin supports TestPlugin interface version 0.1.0, client requires 0.2.0", err.Error())
}

func TestHandshakeFailWrongAPI(t *testing.T) {
testServer, socket := startPluginServer(t)
defer testServer.Stop()

client := rpcClient{client: New(socket, spi.InterfaceSpec{Name: "OtherPlugin", Version: "0.1.0"})}
err := client.DoSomething()
require.Error(t, err)
require.Equal(t, "Plugin does not support interface {OtherPlugin 0.1.0}", err.Error())
}

type rpcClient struct {
client Client
}

func (c rpcClient) DoSomething() error {
req := EmptyMessage{}
resp := EmptyMessage{}
return c.client.Call("TestPlugin.DoSomething", req, &resp)
}

// TestPlugin is an RPC service for this unit test.
type TestPlugin struct {
spec spi.InterfaceSpec
}

// ImplementedInterface returns the interface implemented by this RPC service.
func (p *TestPlugin) ImplementedInterface() spi.InterfaceSpec {
return p.spec
}

// EmptyMessage is an empty test message.
type EmptyMessage struct {
}

// DoSomething is an empty test RPC.
func (p *TestPlugin) DoSomething(_ *http.Request, req *EmptyMessage, resp *EmptyMessage) error {
return nil
}
8 changes: 8 additions & 0 deletions pkg/rpc/client/rpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package client

// Client allows execution of RPCs.
type Client interface {

// Call invokes an RPC method with an argument and a pointer to a result that will hold the return value.
Call(method string, arg interface{}, result interface{}) error
}
2 changes: 1 addition & 1 deletion pkg/rpc/flavor/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

// NewClient returns a plugin interface implementation connected to a remote plugin
func NewClient(socketPath string) flavor.Plugin {
return &client{client: rpc_client.New(socketPath)}
return &client{client: rpc_client.New(socketPath, flavor.InterfaceSpec)}
}

type client struct {
Expand Down
8 changes: 7 additions & 1 deletion pkg/rpc/flavor/service.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package flavor

import (
"github.com/docker/infrakit/pkg/spi"
"github.com/docker/infrakit/pkg/spi/flavor"
"net/http"
)

// PluginServer returns a RPCService that conforms to the net/rpc rpc call convention.
// PluginServer returns a Flavor that conforms to the net/rpc rpc call convention.
func PluginServer(p flavor.Plugin) *Flavor {
return &Flavor{plugin: p}
}
Expand All @@ -15,6 +16,11 @@ type Flavor struct {
plugin flavor.Plugin
}

// ImplementedInterface returns the interface implemented by this RPC service.
func (p *Flavor) ImplementedInterface() spi.InterfaceSpec {
return flavor.InterfaceSpec
}

// Validate checks whether the helper can support a configuration.
func (p *Flavor) Validate(_ *http.Request, req *ValidateRequest, resp *ValidateResponse) error {
err := p.plugin.Validate(*req.Properties, req.Allocation)
Expand Down
2 changes: 1 addition & 1 deletion pkg/rpc/group/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

// NewClient returns a plugin interface implementation connected to a remote plugin
func NewClient(socketPath string) group.Plugin {
return &client{client: rpc_client.New(socketPath)}
return &client{client: rpc_client.New(socketPath, group.InterfaceSpec)}
}

type client struct {
Expand Down
6 changes: 6 additions & 0 deletions pkg/rpc/group/service.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package group

import (
"github.com/docker/infrakit/pkg/spi"
"github.com/docker/infrakit/pkg/spi/group"
"net/http"
)
Expand All @@ -15,6 +16,11 @@ type Group struct {
plugin group.Plugin
}

// ImplementedInterface returns the interface implemented by this RPC service.
func (p *Group) ImplementedInterface() spi.InterfaceSpec {
return group.InterfaceSpec
}

// CommitGroup is the rpc method to commit a group
func (p *Group) CommitGroup(_ *http.Request, req *CommitGroupRequest, resp *CommitGroupResponse) error {
details, err := p.plugin.CommitGroup(req.Spec, req.Pretend)
Expand Down
2 changes: 1 addition & 1 deletion pkg/rpc/instance/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

// NewClient returns a plugin interface implementation connected to a plugin
func NewClient(socketPath string) instance.Plugin {
return &client{client: rpc_client.New(socketPath)}
return &client{client: rpc_client.New(socketPath, instance.InterfaceSpec)}
}

type client struct {
Expand Down
6 changes: 6 additions & 0 deletions pkg/rpc/instance/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package instance

import (
"errors"
"github.com/docker/infrakit/pkg/spi"
"github.com/docker/infrakit/pkg/spi/instance"
"net/http"
)
Expand All @@ -17,6 +18,11 @@ type Instance struct {
plugin instance.Plugin
}

// ImplementedInterface returns the interface implemented by this RPC service.
func (p *Instance) ImplementedInterface() spi.InterfaceSpec {
return instance.InterfaceSpec
}

// Validate performs local validation on a provision request.
func (p *Instance) Validate(_ *http.Request, req *ValidateRequest, resp *ValidateResponse) error {
if req.Properties == nil {
Expand Down
17 changes: 17 additions & 0 deletions pkg/rpc/plugin/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package plugin

import (
"github.com/docker/infrakit/pkg/spi"
"net/http"
)

// Plugin is the service for API metadata.
type Plugin struct {
Spec spi.InterfaceSpec
}

// Implements responds to a request for the supported plugin interfaces.
func (p Plugin) Implements(_ *http.Request, req *ImplementsRequest, resp *ImplementsResponse) error {
resp.APIs = []spi.InterfaceSpec{p.Spec}
return nil
}
Loading

0 comments on commit eab86f0

Please sign in to comment.