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

Commit eab86f0

Browse files
wfarnerDavid Chung
authored andcommitted
Add Version information to plugin APIs (#318)
Signed-off-by: Bill Farner <[email protected]>
1 parent a1e94af commit eab86f0

File tree

22 files changed

+294
-21
lines changed

22 files changed

+294
-21
lines changed

docs/plugins/README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,15 @@ _InfraKit_ plugins are exposed via HTTP, using [JSON-RPC 2.0](http://www.jsonrpc
113113
API requests can be made manually with `curl`. For example, the following command will list all groups:
114114
```console
115115
$ curl -X POST --unix-socket ~/.infrakit/plugins/group http:/rpc \
116-
-H 'Content-Type: application/json'
116+
-H 'Content-Type: application/json' \
117117
-d '{"jsonrpc":"2.0","method":"Group.InspectGroups","params":{},"id":1}'
118118
{"jsonrpc":"2.0","result":{"Groups":null},"id":1}
119119
```
120120

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

137137
Additionally, all plugins will log each API HTTP request and response when run with the `--log 5` command line argument.
138+
139+
##### API identification
140+
Plugins are required to identify the name and version of plugin APIs they implement. This is done with a request
141+
like the following:
142+
143+
```console
144+
$ curl -X POST --unix-socket ~/.infrakit/plugins/group http:/rpc \
145+
-H 'Content-Type: application/json' \
146+
-d '{"jsonrpc":"2.0","method":"Plugin.Implements","params":{},"id":1}'
147+
{"jsonrpc":"2.0","result":{"Interfaces":[{"Name":"Group","Version":"0.1.0"}]},"id":1}
148+
```

docs/plugins/flavor.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Flavor plugin API
22

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

55
## API
66

docs/plugins/group.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Group plugin API
22

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

55
## API
66

docs/plugins/instance.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Instance plugin API
22

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

55
## API
66

pkg/cli/serverutil.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

1111
// RunPlugin runs a plugin server, advertising with the provided name for discovery.
1212
// The plugin should conform to the rpc call convention as implemented in the rpc package.
13-
func RunPlugin(name string, plugin interface{}) {
13+
func RunPlugin(name string, plugin server.VersionedInterface) {
1414
stoppable, err := server.StartPluginAtPath(path.Join(discovery.Dir(), name), plugin)
1515
if err != nil {
1616
log.Error(err)

pkg/rpc/client/client.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,35 @@ package client
33
import (
44
"bytes"
55
log "github.com/Sirupsen/logrus"
6+
"github.com/docker/infrakit/pkg/spi"
67
"github.com/gorilla/rpc/v2/json2"
78
"net"
89
"net/http"
910
"net/http/httputil"
11+
"sync"
1012
)
1113

12-
// Client is an HTTP client for sending JSON-RPC requests.
13-
type Client struct {
14+
type client struct {
1415
http http.Client
1516
}
1617

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

23-
return Client{http: http.Client{Transport: &http.Transport{Dial: dialUnix}}}
24+
unvalidatedClient := &client{http: http.Client{Transport: &http.Transport{Dial: dialUnix}}}
25+
return &handshakingClient{client: unvalidatedClient, iface: api, lock: &sync.Mutex{}}
2426
}
2527

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

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

46-
resp, err := c.http.Post("http://d/rpc", "application/json", bytes.NewReader(message))
47+
resp, err := c.http.Do(req)
4748
if err != nil {
4849
return err
4950
}

pkg/rpc/client/handshake.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package client
2+
3+
import (
4+
"fmt"
5+
"github.com/docker/infrakit/pkg/rpc/plugin"
6+
"github.com/docker/infrakit/pkg/spi"
7+
"sync"
8+
)
9+
10+
type handshakingClient struct {
11+
client Client
12+
iface spi.InterfaceSpec
13+
14+
// handshakeResult handles the tri-state outcome of handshake state:
15+
// - handshake has not yet completed (nil)
16+
// - handshake completed successfully (non-nil result, nil error)
17+
// - handshake failed (non-nil result, non-nil error)
18+
handshakeResult *handshakeResult
19+
20+
// lock guards handshakeResult
21+
lock *sync.Mutex
22+
}
23+
24+
type handshakeResult struct {
25+
err error
26+
}
27+
28+
func (c *handshakingClient) handshake() error {
29+
c.lock.Lock()
30+
defer c.lock.Unlock()
31+
32+
if c.handshakeResult == nil {
33+
req := plugin.ImplementsRequest{}
34+
resp := plugin.ImplementsResponse{}
35+
36+
if err := c.client.Call("Plugin.Implements", req, &resp); err != nil {
37+
return err
38+
}
39+
40+
err := fmt.Errorf("Plugin does not support interface %v", c.iface)
41+
if resp.APIs != nil {
42+
for _, iface := range resp.APIs {
43+
if iface.Name == c.iface.Name {
44+
if iface.Version == c.iface.Version {
45+
err = nil
46+
break
47+
} else {
48+
err = fmt.Errorf(
49+
"Plugin supports %s interface version %s, client requires %s",
50+
iface.Name,
51+
iface.Version,
52+
c.iface.Version)
53+
}
54+
}
55+
}
56+
}
57+
58+
c.handshakeResult = &handshakeResult{err: err}
59+
}
60+
61+
return c.handshakeResult.err
62+
}
63+
64+
func (c *handshakingClient) Call(method string, arg interface{}, result interface{}) error {
65+
if err := c.handshake(); err != nil {
66+
return err
67+
}
68+
69+
return c.client.Call(method, arg, result)
70+
}

pkg/rpc/client/handshake_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package client
2+
3+
import (
4+
"github.com/docker/infrakit/pkg/rpc/server"
5+
"github.com/docker/infrakit/pkg/spi"
6+
"github.com/stretchr/testify/require"
7+
"io/ioutil"
8+
"net/http"
9+
"path/filepath"
10+
"testing"
11+
)
12+
13+
var apiSpec = spi.InterfaceSpec{
14+
Name: "TestPlugin",
15+
Version: "0.1.0",
16+
}
17+
18+
func startPluginServer(t *testing.T) (server.Stoppable, string) {
19+
dir, err := ioutil.TempDir("", "infrakit_handshake_test")
20+
require.NoError(t, err)
21+
22+
name := "instance"
23+
socket := filepath.Join(dir, name)
24+
25+
testServer, err := server.StartPluginAtPath(socket, &TestPlugin{spec: apiSpec})
26+
require.NoError(t, err)
27+
return testServer, socket
28+
}
29+
30+
func TestHandshakeSuccess(t *testing.T) {
31+
testServer, socket := startPluginServer(t)
32+
defer testServer.Stop()
33+
34+
client := rpcClient{client: New(socket, apiSpec)}
35+
require.NoError(t, client.DoSomething())
36+
}
37+
38+
func TestHandshakeFailVersion(t *testing.T) {
39+
testServer, socket := startPluginServer(t)
40+
defer testServer.Stop()
41+
42+
client := rpcClient{client: New(socket, spi.InterfaceSpec{Name: "TestPlugin", Version: "0.2.0"})}
43+
err := client.DoSomething()
44+
require.Error(t, err)
45+
require.Equal(t, "Plugin supports TestPlugin interface version 0.1.0, client requires 0.2.0", err.Error())
46+
}
47+
48+
func TestHandshakeFailWrongAPI(t *testing.T) {
49+
testServer, socket := startPluginServer(t)
50+
defer testServer.Stop()
51+
52+
client := rpcClient{client: New(socket, spi.InterfaceSpec{Name: "OtherPlugin", Version: "0.1.0"})}
53+
err := client.DoSomething()
54+
require.Error(t, err)
55+
require.Equal(t, "Plugin does not support interface {OtherPlugin 0.1.0}", err.Error())
56+
}
57+
58+
type rpcClient struct {
59+
client Client
60+
}
61+
62+
func (c rpcClient) DoSomething() error {
63+
req := EmptyMessage{}
64+
resp := EmptyMessage{}
65+
return c.client.Call("TestPlugin.DoSomething", req, &resp)
66+
}
67+
68+
// TestPlugin is an RPC service for this unit test.
69+
type TestPlugin struct {
70+
spec spi.InterfaceSpec
71+
}
72+
73+
// ImplementedInterface returns the interface implemented by this RPC service.
74+
func (p *TestPlugin) ImplementedInterface() spi.InterfaceSpec {
75+
return p.spec
76+
}
77+
78+
// EmptyMessage is an empty test message.
79+
type EmptyMessage struct {
80+
}
81+
82+
// DoSomething is an empty test RPC.
83+
func (p *TestPlugin) DoSomething(_ *http.Request, req *EmptyMessage, resp *EmptyMessage) error {
84+
return nil
85+
}

pkg/rpc/client/rpc.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package client
2+
3+
// Client allows execution of RPCs.
4+
type Client interface {
5+
6+
// Call invokes an RPC method with an argument and a pointer to a result that will hold the return value.
7+
Call(method string, arg interface{}, result interface{}) error
8+
}

pkg/rpc/flavor/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

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

1616
type client struct {

0 commit comments

Comments
 (0)