Initial commit

This commit is contained in:
Ria Bhatia
2017-12-04 13:32:57 -06:00
committed by Erik St. Martin
commit 0075e5b0f3
9056 changed files with 2523100 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
package plugins
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig"
)
const (
versionMimetype = "application/vnd.docker.plugins.v1.2+json"
defaultTimeOut = 30
)
// NewClient creates a new plugin client (http).
func NewClient(addr string, tlsConfig tlsconfig.Options) (*Client, error) {
tr := &http.Transport{}
c, err := tlsconfig.Client(tlsConfig)
if err != nil {
return nil, err
}
tr.TLSClientConfig = c
protoAndAddr := strings.Split(addr, "://")
sockets.ConfigureTCPTransport(tr, protoAndAddr[0], protoAndAddr[1])
scheme := protoAndAddr[0]
if scheme != "https" {
scheme = "http"
}
return &Client{&http.Client{Transport: tr}, scheme, protoAndAddr[1]}, nil
}
// Client represents a plugin client.
type Client struct {
http *http.Client // http client to use
scheme string // scheme protocol of the plugin
addr string // http address of the plugin
}
// Call calls the specified method with the specified arguments for the plugin.
// It will retry for 30 seconds if a failure occurs when calling.
func (c *Client) Call(serviceMethod string, args interface{}, ret interface{}) error {
var buf bytes.Buffer
if args != nil {
if err := json.NewEncoder(&buf).Encode(args); err != nil {
return err
}
}
body, err := c.callWithRetry(serviceMethod, &buf, true)
if err != nil {
return err
}
defer body.Close()
if ret != nil {
if err := json.NewDecoder(body).Decode(&ret); err != nil {
logrus.Errorf("%s: error reading plugin resp: %v", serviceMethod, err)
return err
}
}
return nil
}
// Stream calls the specified method with the specified arguments for the plugin and returns the response body
func (c *Client) Stream(serviceMethod string, args interface{}) (io.ReadCloser, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(args); err != nil {
return nil, err
}
return c.callWithRetry(serviceMethod, &buf, true)
}
// SendFile calls the specified method, and passes through the IO stream
func (c *Client) SendFile(serviceMethod string, data io.Reader, ret interface{}) error {
body, err := c.callWithRetry(serviceMethod, data, true)
if err != nil {
return err
}
if err := json.NewDecoder(body).Decode(&ret); err != nil {
logrus.Errorf("%s: error reading plugin resp: %v", serviceMethod, err)
return err
}
return nil
}
func (c *Client) callWithRetry(serviceMethod string, data io.Reader, retry bool) (io.ReadCloser, error) {
req, err := http.NewRequest("POST", "/"+serviceMethod, data)
if err != nil {
return nil, err
}
req.Header.Add("Accept", versionMimetype)
req.URL.Scheme = c.scheme
req.URL.Host = c.addr
var retries int
start := time.Now()
for {
resp, err := c.http.Do(req)
if err != nil {
if !retry {
return nil, err
}
timeOff := backoff(retries)
if abort(start, timeOff) {
return nil, err
}
retries++
logrus.Warnf("Unable to connect to plugin: %s, retrying in %v", c.addr, timeOff)
time.Sleep(timeOff)
continue
}
if resp.StatusCode != http.StatusOK {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &statusError{resp.StatusCode, serviceMethod, err.Error()}
}
// Plugins' Response(s) should have an Err field indicating what went
// wrong. Try to unmarshal into ResponseErr. Otherwise fallback to just
// return the string(body)
type responseErr struct {
Err string
}
remoteErr := responseErr{}
if err := json.Unmarshal(b, &remoteErr); err == nil {
if remoteErr.Err != "" {
return nil, &statusError{resp.StatusCode, serviceMethod, remoteErr.Err}
}
}
// old way...
return nil, &statusError{resp.StatusCode, serviceMethod, string(b)}
}
return resp.Body, nil
}
}
func backoff(retries int) time.Duration {
b, max := 1, defaultTimeOut
for b < max && retries > 0 {
b *= 2
retries--
}
if b > max {
b = max
}
return time.Duration(b) * time.Second
}
func abort(start time.Time, timeOff time.Duration) bool {
return timeOff+time.Since(start) >= time.Duration(defaultTimeOut)*time.Second
}

View File

@@ -0,0 +1,127 @@
package plugins
import (
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"github.com/docker/go-connections/tlsconfig"
)
var (
mux *http.ServeMux
server *httptest.Server
)
func setupRemotePluginServer() string {
mux = http.NewServeMux()
server = httptest.NewServer(mux)
return server.URL
}
func teardownRemotePluginServer() {
if server != nil {
server.Close()
}
}
func TestFailedConnection(t *testing.T) {
c, _ := NewClient("tcp://127.0.0.1:1", tlsconfig.Options{InsecureSkipVerify: true})
_, err := c.callWithRetry("Service.Method", nil, false)
if err == nil {
t.Fatal("Unexpected successful connection")
}
}
func TestEchoInputOutput(t *testing.T) {
addr := setupRemotePluginServer()
defer teardownRemotePluginServer()
m := Manifest{[]string{"VolumeDriver", "NetworkDriver"}}
mux.HandleFunc("/Test.Echo", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
t.Fatalf("Expected POST, got %s\n", r.Method)
}
header := w.Header()
header.Set("Content-Type", versionMimetype)
io.Copy(w, r.Body)
})
c, _ := NewClient(addr, tlsconfig.Options{InsecureSkipVerify: true})
var output Manifest
err := c.Call("Test.Echo", m, &output)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(output, m) {
t.Fatalf("Expected %v, was %v\n", m, output)
}
err = c.Call("Test.Echo", nil, nil)
if err != nil {
t.Fatal(err)
}
}
func TestBackoff(t *testing.T) {
cases := []struct {
retries int
expTimeOff time.Duration
}{
{0, time.Duration(1)},
{1, time.Duration(2)},
{2, time.Duration(4)},
{4, time.Duration(16)},
{6, time.Duration(30)},
{10, time.Duration(30)},
}
for _, c := range cases {
s := c.expTimeOff * time.Second
if d := backoff(c.retries); d != s {
t.Fatalf("Retry %v, expected %v, was %v\n", c.retries, s, d)
}
}
}
func TestAbortRetry(t *testing.T) {
cases := []struct {
timeOff time.Duration
expAbort bool
}{
{time.Duration(1), false},
{time.Duration(2), false},
{time.Duration(10), false},
{time.Duration(30), true},
{time.Duration(40), true},
}
for _, c := range cases {
s := c.timeOff * time.Second
if a := abort(time.Now(), s); a != c.expAbort {
t.Fatalf("Duration %v, expected %v, was %v\n", c.timeOff, s, a)
}
}
}
func TestClientScheme(t *testing.T) {
cases := map[string]string{
"tcp://127.0.0.1:8080": "http",
"unix:///usr/local/plugins/foo": "http",
"http://127.0.0.1:8080": "http",
"https://127.0.0.1:8080": "https",
}
for addr, scheme := range cases {
c, _ := NewClient(addr, tlsconfig.Options{InsecureSkipVerify: true})
if c.scheme != scheme {
t.Fatalf("URL scheme mismatch, expected %s, got %s", scheme, c.scheme)
}
}
}

View File

@@ -0,0 +1,130 @@
package plugins
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
)
var (
// ErrNotFound plugin not found
ErrNotFound = errors.New("plugin not found")
socketsPath = "/run/docker/plugins"
specsPaths = []string{"/etc/docker/plugins", "/usr/lib/docker/plugins"}
)
// localRegistry defines a registry that is local (using unix socket).
type localRegistry struct{}
func newLocalRegistry() localRegistry {
return localRegistry{}
}
// Scan scans all the plugin paths and returns all the names it found
func Scan() ([]string, error) {
var names []string
if err := filepath.Walk(socketsPath, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return nil
}
if fi.Mode()&os.ModeSocket != 0 {
name := strings.TrimSuffix(fi.Name(), filepath.Ext(fi.Name()))
names = append(names, name)
}
return nil
}); err != nil {
return nil, err
}
for _, path := range specsPaths {
if err := filepath.Walk(path, func(p string, fi os.FileInfo, err error) error {
if err != nil || fi.IsDir() {
return nil
}
name := strings.TrimSuffix(fi.Name(), filepath.Ext(fi.Name()))
names = append(names, name)
return nil
}); err != nil {
return nil, err
}
}
return names, nil
}
// Plugin returns the plugin registered with the given name (or returns an error).
func (l *localRegistry) Plugin(name string) (*Plugin, error) {
socketpaths := pluginPaths(socketsPath, name, ".sock")
for _, p := range socketpaths {
if fi, err := os.Stat(p); err == nil && fi.Mode()&os.ModeSocket != 0 {
return newLocalPlugin(name, "unix://"+p), nil
}
}
var txtspecpaths []string
for _, p := range specsPaths {
txtspecpaths = append(txtspecpaths, pluginPaths(p, name, ".spec")...)
txtspecpaths = append(txtspecpaths, pluginPaths(p, name, ".json")...)
}
for _, p := range txtspecpaths {
if _, err := os.Stat(p); err == nil {
if strings.HasSuffix(p, ".json") {
return readPluginJSONInfo(name, p)
}
return readPluginInfo(name, p)
}
}
return nil, ErrNotFound
}
func readPluginInfo(name, path string) (*Plugin, error) {
content, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
addr := strings.TrimSpace(string(content))
u, err := url.Parse(addr)
if err != nil {
return nil, err
}
if len(u.Scheme) == 0 {
return nil, fmt.Errorf("Unknown protocol")
}
return newLocalPlugin(name, addr), nil
}
func readPluginJSONInfo(name, path string) (*Plugin, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var p Plugin
if err := json.NewDecoder(f).Decode(&p); err != nil {
return nil, err
}
p.Name = name
if len(p.TLSConfig.CAFile) == 0 {
p.TLSConfig.InsecureSkipVerify = true
}
return &p, nil
}
func pluginPaths(base, name, ext string) []string {
return []string{
filepath.Join(base, name+ext),
filepath.Join(base, name, name+ext),
}
}

View File

@@ -0,0 +1,169 @@
package plugins
import (
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"reflect"
"testing"
)
func setup(t *testing.T) (string, func()) {
tmpdir, err := ioutil.TempDir("", "docker-test")
if err != nil {
t.Fatal(err)
}
backup := socketsPath
socketsPath = tmpdir
specsPaths = []string{tmpdir}
return tmpdir, func() {
socketsPath = backup
os.RemoveAll(tmpdir)
}
}
func TestLocalSocket(t *testing.T) {
tmpdir, unregister := setup(t)
defer unregister()
cases := []string{
filepath.Join(tmpdir, "echo.sock"),
filepath.Join(tmpdir, "echo", "echo.sock"),
}
for _, c := range cases {
if err := os.MkdirAll(filepath.Dir(c), 0755); err != nil {
t.Fatal(err)
}
l, err := net.Listen("unix", c)
if err != nil {
t.Fatal(err)
}
r := newLocalRegistry()
p, err := r.Plugin("echo")
if err != nil {
t.Fatal(err)
}
pp, err := r.Plugin("echo")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(p, pp) {
t.Fatalf("Expected %v, was %v\n", p, pp)
}
if p.Name != "echo" {
t.Fatalf("Expected plugin `echo`, got %s\n", p.Name)
}
addr := fmt.Sprintf("unix://%s", c)
if p.Addr != addr {
t.Fatalf("Expected plugin addr `%s`, got %s\n", addr, p.Addr)
}
if p.TLSConfig.InsecureSkipVerify != true {
t.Fatalf("Expected TLS verification to be skipped")
}
l.Close()
}
}
func TestFileSpecPlugin(t *testing.T) {
tmpdir, unregister := setup(t)
defer unregister()
cases := []struct {
path string
name string
addr string
fail bool
}{
{filepath.Join(tmpdir, "echo.spec"), "echo", "unix://var/lib/docker/plugins/echo.sock", false},
{filepath.Join(tmpdir, "echo", "echo.spec"), "echo", "unix://var/lib/docker/plugins/echo.sock", false},
{filepath.Join(tmpdir, "foo.spec"), "foo", "tcp://localhost:8080", false},
{filepath.Join(tmpdir, "foo", "foo.spec"), "foo", "tcp://localhost:8080", false},
{filepath.Join(tmpdir, "bar.spec"), "bar", "localhost:8080", true}, // unknown transport
}
for _, c := range cases {
if err := os.MkdirAll(filepath.Dir(c.path), 0755); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(c.path, []byte(c.addr), 0644); err != nil {
t.Fatal(err)
}
r := newLocalRegistry()
p, err := r.Plugin(c.name)
if c.fail && err == nil {
continue
}
if err != nil {
t.Fatal(err)
}
if p.Name != c.name {
t.Fatalf("Expected plugin `%s`, got %s\n", c.name, p.Name)
}
if p.Addr != c.addr {
t.Fatalf("Expected plugin addr `%s`, got %s\n", c.addr, p.Addr)
}
if p.TLSConfig.InsecureSkipVerify != true {
t.Fatalf("Expected TLS verification to be skipped")
}
}
}
func TestFileJSONSpecPlugin(t *testing.T) {
tmpdir, unregister := setup(t)
defer unregister()
p := filepath.Join(tmpdir, "example.json")
spec := `{
"Name": "plugin-example",
"Addr": "https://example.com/docker/plugin",
"TLSConfig": {
"CAFile": "/usr/shared/docker/certs/example-ca.pem",
"CertFile": "/usr/shared/docker/certs/example-cert.pem",
"KeyFile": "/usr/shared/docker/certs/example-key.pem"
}
}`
if err := ioutil.WriteFile(p, []byte(spec), 0644); err != nil {
t.Fatal(err)
}
r := newLocalRegistry()
plugin, err := r.Plugin("example")
if err != nil {
t.Fatal(err)
}
if plugin.Name != "example" {
t.Fatalf("Expected plugin `plugin-example`, got %s\n", plugin.Name)
}
if plugin.Addr != "https://example.com/docker/plugin" {
t.Fatalf("Expected plugin addr `https://example.com/docker/plugin`, got %s\n", plugin.Addr)
}
if plugin.TLSConfig.CAFile != "/usr/shared/docker/certs/example-ca.pem" {
t.Fatalf("Expected plugin CA `/usr/shared/docker/certs/example-ca.pem`, got %s\n", plugin.TLSConfig.CAFile)
}
if plugin.TLSConfig.CertFile != "/usr/shared/docker/certs/example-cert.pem" {
t.Fatalf("Expected plugin Certificate `/usr/shared/docker/certs/example-cert.pem`, got %s\n", plugin.TLSConfig.CertFile)
}
if plugin.TLSConfig.KeyFile != "/usr/shared/docker/certs/example-key.pem" {
t.Fatalf("Expected plugin Key `/usr/shared/docker/certs/example-key.pem`, got %s\n", plugin.TLSConfig.KeyFile)
}
}

View File

@@ -0,0 +1,33 @@
package plugins
import (
"fmt"
"net/http"
)
type statusError struct {
status int
method string
err string
}
// Error returns a formated string for this error type
func (e *statusError) Error() string {
return fmt.Sprintf("%s: %v", e.method, e.err)
}
// IsNotFound indicates if the passed in error is from an http.StatusNotFound from the plugin
func IsNotFound(err error) bool {
return isStatusError(err, http.StatusNotFound)
}
func isStatusError(err error, status int) bool {
if err == nil {
return false
}
e, ok := err.(*statusError)
if !ok {
return false
}
return e.status == status
}

View File

@@ -0,0 +1,68 @@
Plugin RPC Generator
====================
Generates go code from a Go interface definition for proxying between the plugin
API and the subsystem being extended.
## Usage
Given an interface definition:
```go
type volumeDriver interface {
Create(name string, opts opts) (err error)
Remove(name string) (err error)
Path(name string) (mountpoint string, err error)
Mount(name string) (mountpoint string, err error)
Unmount(name string) (err error)
}
```
**Note**: All function options and return values must be named in the definition.
Run the generator:
```bash
$ pluginrpc-gen --type volumeDriver --name VolumeDriver -i volumes/drivers/extpoint.go -o volumes/drivers/proxy.go
```
Where:
- `--type` is the name of the interface to use
- `--name` is the subsystem that the plugin "Implements"
- `-i` is the input file containing the interface definition
- `-o` is the output file where the the generated code should go
**Note**: The generated code will use the same package name as the one defined in the input file
Optionally, you can skip functions on the interface that should not be
implemented in the generated proxy code by passing in the function name to `--skip`.
This flag can be specified multiple times.
You can also add build tags that should be prepended to the generated code by
supplying `--tag`. This flag can be specified multiple times.
## Known issues
The parser can currently only handle types which are not specifically a map or
a slice.
You can, however, create a type that uses a map or a slice internally, for instance:
```go
type opts map[string]string
```
This `opts` type will work, whreas using a `map[string]string` directly will not.
## go-generate
You can also use this with go-generate, which is pretty awesome.
To do so, place the code at the top of the file which contains the interface
definition (i.e., the input file):
```go
//go:generate pluginrpc-gen -i $GOFILE -o proxy.go -type volumeDriver -name VolumeDriver
```
Then cd to the package dir and run `go generate`
**Note**: the `pluginrpc-gen` binary must be within your `$PATH`

View File

@@ -0,0 +1,41 @@
package foo
type wobble struct {
Some string
Val string
Inception *wobble
}
// Fooer is an empty interface used for tests.
type Fooer interface{}
// Fooer2 is an interface used for tests.
type Fooer2 interface {
Foo()
}
// Fooer3 is an interface used for tests.
type Fooer3 interface {
Foo()
Bar(a string)
Baz(a string) (err error)
Qux(a, b string) (val string, err error)
Wobble() (w *wobble)
Wiggle() (w wobble)
}
// Fooer4 is an interface used for tests.
type Fooer4 interface {
Foo() error
}
// Bar is an interface used for tests.
type Bar interface {
Boo(a string, b string) (s string, err error)
}
// Fooer5 is an interface used for tests.
type Fooer5 interface {
Foo()
Bar
}

View File

@@ -0,0 +1,91 @@
package main
import (
"bytes"
"flag"
"fmt"
"go/format"
"io/ioutil"
"os"
"unicode"
"unicode/utf8"
)
type stringSet struct {
values map[string]struct{}
}
func (s stringSet) String() string {
return ""
}
func (s stringSet) Set(value string) error {
s.values[value] = struct{}{}
return nil
}
func (s stringSet) GetValues() map[string]struct{} {
return s.values
}
var (
typeName = flag.String("type", "", "interface type to generate plugin rpc proxy for")
rpcName = flag.String("name", *typeName, "RPC name, set if different from type")
inputFile = flag.String("i", "", "input file path")
outputFile = flag.String("o", *inputFile+"_proxy.go", "output file path")
skipFuncs map[string]struct{}
flSkipFuncs = stringSet{make(map[string]struct{})}
flBuildTags = stringSet{make(map[string]struct{})}
)
func errorOut(msg string, err error) {
if err == nil {
return
}
fmt.Fprintf(os.Stderr, "%s: %v\n", msg, err)
os.Exit(1)
}
func checkFlags() error {
if *outputFile == "" {
return fmt.Errorf("missing required flag `-o`")
}
if *inputFile == "" {
return fmt.Errorf("missing required flag `-i`")
}
return nil
}
func main() {
flag.Var(flSkipFuncs, "skip", "skip parsing for function")
flag.Var(flBuildTags, "tag", "build tags to add to generated files")
flag.Parse()
skipFuncs = flSkipFuncs.GetValues()
errorOut("error", checkFlags())
pkg, err := Parse(*inputFile, *typeName)
errorOut(fmt.Sprintf("error parsing requested type %s", *typeName), err)
var analysis = struct {
InterfaceType string
RPCName string
BuildTags map[string]struct{}
*ParsedPkg
}{toLower(*typeName), *rpcName, flBuildTags.GetValues(), pkg}
var buf bytes.Buffer
errorOut("parser error", generatedTempl.Execute(&buf, analysis))
src, err := format.Source(buf.Bytes())
errorOut("error formating generated source", err)
errorOut("error writing file", ioutil.WriteFile(*outputFile, src, 0644))
}
func toLower(s string) string {
if s == "" {
return ""
}
r, n := utf8.DecodeRuneInString(s)
return string(unicode.ToLower(r)) + s[n:]
}

View File

@@ -0,0 +1,163 @@
package main
import (
"errors"
"fmt"
"go/ast"
"go/parser"
"go/token"
"reflect"
)
var errBadReturn = errors.New("found return arg with no name: all args must be named")
type errUnexpectedType struct {
expected string
actual interface{}
}
func (e errUnexpectedType) Error() string {
return fmt.Sprintf("got wrong type expecting %s, got: %v", e.expected, reflect.TypeOf(e.actual))
}
// ParsedPkg holds information about a package that has been parsed,
// its name and the list of functions.
type ParsedPkg struct {
Name string
Functions []function
}
type function struct {
Name string
Args []arg
Returns []arg
Doc string
}
type arg struct {
Name string
ArgType string
}
func (a *arg) String() string {
return a.Name + " " + a.ArgType
}
// Parse parses the given file for an interface definition with the given name.
func Parse(filePath string, objName string) (*ParsedPkg, error) {
fs := token.NewFileSet()
pkg, err := parser.ParseFile(fs, filePath, nil, parser.AllErrors)
if err != nil {
return nil, err
}
p := &ParsedPkg{}
p.Name = pkg.Name.Name
obj, exists := pkg.Scope.Objects[objName]
if !exists {
return nil, fmt.Errorf("could not find object %s in %s", objName, filePath)
}
if obj.Kind != ast.Typ {
return nil, fmt.Errorf("exected type, got %s", obj.Kind)
}
spec, ok := obj.Decl.(*ast.TypeSpec)
if !ok {
return nil, errUnexpectedType{"*ast.TypeSpec", obj.Decl}
}
iface, ok := spec.Type.(*ast.InterfaceType)
if !ok {
return nil, errUnexpectedType{"*ast.InterfaceType", spec.Type}
}
p.Functions, err = parseInterface(iface)
if err != nil {
return nil, err
}
return p, nil
}
func parseInterface(iface *ast.InterfaceType) ([]function, error) {
var functions []function
for _, field := range iface.Methods.List {
switch f := field.Type.(type) {
case *ast.FuncType:
method, err := parseFunc(field)
if err != nil {
return nil, err
}
if method == nil {
continue
}
functions = append(functions, *method)
case *ast.Ident:
spec, ok := f.Obj.Decl.(*ast.TypeSpec)
if !ok {
return nil, errUnexpectedType{"*ast.TypeSpec", f.Obj.Decl}
}
iface, ok := spec.Type.(*ast.InterfaceType)
if !ok {
return nil, errUnexpectedType{"*ast.TypeSpec", spec.Type}
}
funcs, err := parseInterface(iface)
if err != nil {
fmt.Println(err)
continue
}
functions = append(functions, funcs...)
default:
return nil, errUnexpectedType{"*astFuncType or *ast.Ident", f}
}
}
return functions, nil
}
func parseFunc(field *ast.Field) (*function, error) {
f := field.Type.(*ast.FuncType)
method := &function{Name: field.Names[0].Name}
if _, exists := skipFuncs[method.Name]; exists {
fmt.Println("skipping:", method.Name)
return nil, nil
}
if f.Params != nil {
args, err := parseArgs(f.Params.List)
if err != nil {
return nil, err
}
method.Args = args
}
if f.Results != nil {
returns, err := parseArgs(f.Results.List)
if err != nil {
return nil, fmt.Errorf("error parsing function returns for %q: %v", method.Name, err)
}
method.Returns = returns
}
return method, nil
}
func parseArgs(fields []*ast.Field) ([]arg, error) {
var args []arg
for _, f := range fields {
if len(f.Names) == 0 {
return nil, errBadReturn
}
for _, name := range f.Names {
var typeName string
switch argType := f.Type.(type) {
case *ast.Ident:
typeName = argType.Name
case *ast.StarExpr:
i, ok := argType.X.(*ast.Ident)
if !ok {
return nil, errUnexpectedType{"*ast.Ident", f.Type}
}
typeName = "*" + i.Name
default:
return nil, errUnexpectedType{"*ast.Ident or *ast.StarExpr", f.Type}
}
args = append(args, arg{name.Name, typeName})
}
}
return args, nil
}

View File

@@ -0,0 +1,168 @@
package main
import (
"fmt"
"path/filepath"
"runtime"
"strings"
"testing"
)
const testFixture = "fixtures/foo.go"
func TestParseEmptyInterface(t *testing.T) {
pkg, err := Parse(testFixture, "Fooer")
if err != nil {
t.Fatal(err)
}
assertName(t, "foo", pkg.Name)
assertNum(t, 0, len(pkg.Functions))
}
func TestParseNonInterfaceType(t *testing.T) {
_, err := Parse(testFixture, "wobble")
if _, ok := err.(errUnexpectedType); !ok {
t.Fatal("expected type error when parsing non-interface type")
}
}
func TestParseWithOneFunction(t *testing.T) {
pkg, err := Parse(testFixture, "Fooer2")
if err != nil {
t.Fatal(err)
}
assertName(t, "foo", pkg.Name)
assertNum(t, 1, len(pkg.Functions))
assertName(t, "Foo", pkg.Functions[0].Name)
assertNum(t, 0, len(pkg.Functions[0].Args))
assertNum(t, 0, len(pkg.Functions[0].Returns))
}
func TestParseWithMultipleFuncs(t *testing.T) {
pkg, err := Parse(testFixture, "Fooer3")
if err != nil {
t.Fatal(err)
}
assertName(t, "foo", pkg.Name)
assertNum(t, 6, len(pkg.Functions))
f := pkg.Functions[0]
assertName(t, "Foo", f.Name)
assertNum(t, 0, len(f.Args))
assertNum(t, 0, len(f.Returns))
f = pkg.Functions[1]
assertName(t, "Bar", f.Name)
assertNum(t, 1, len(f.Args))
assertNum(t, 0, len(f.Returns))
arg := f.Args[0]
assertName(t, "a", arg.Name)
assertName(t, "string", arg.ArgType)
f = pkg.Functions[2]
assertName(t, "Baz", f.Name)
assertNum(t, 1, len(f.Args))
assertNum(t, 1, len(f.Returns))
arg = f.Args[0]
assertName(t, "a", arg.Name)
assertName(t, "string", arg.ArgType)
arg = f.Returns[0]
assertName(t, "err", arg.Name)
assertName(t, "error", arg.ArgType)
f = pkg.Functions[3]
assertName(t, "Qux", f.Name)
assertNum(t, 2, len(f.Args))
assertNum(t, 2, len(f.Returns))
arg = f.Args[0]
assertName(t, "a", f.Args[0].Name)
assertName(t, "string", f.Args[0].ArgType)
arg = f.Args[1]
assertName(t, "b", arg.Name)
assertName(t, "string", arg.ArgType)
arg = f.Returns[0]
assertName(t, "val", arg.Name)
assertName(t, "string", arg.ArgType)
arg = f.Returns[1]
assertName(t, "err", arg.Name)
assertName(t, "error", arg.ArgType)
f = pkg.Functions[4]
assertName(t, "Wobble", f.Name)
assertNum(t, 0, len(f.Args))
assertNum(t, 1, len(f.Returns))
arg = f.Returns[0]
assertName(t, "w", arg.Name)
assertName(t, "*wobble", arg.ArgType)
f = pkg.Functions[5]
assertName(t, "Wiggle", f.Name)
assertNum(t, 0, len(f.Args))
assertNum(t, 1, len(f.Returns))
arg = f.Returns[0]
assertName(t, "w", arg.Name)
assertName(t, "wobble", arg.ArgType)
}
func TestParseWithUnamedReturn(t *testing.T) {
_, err := Parse(testFixture, "Fooer4")
if !strings.HasSuffix(err.Error(), errBadReturn.Error()) {
t.Fatalf("expected ErrBadReturn, got %v", err)
}
}
func TestEmbeddedInterface(t *testing.T) {
pkg, err := Parse(testFixture, "Fooer5")
if err != nil {
t.Fatal(err)
}
assertName(t, "foo", pkg.Name)
assertNum(t, 2, len(pkg.Functions))
f := pkg.Functions[0]
assertName(t, "Foo", f.Name)
assertNum(t, 0, len(f.Args))
assertNum(t, 0, len(f.Returns))
f = pkg.Functions[1]
assertName(t, "Boo", f.Name)
assertNum(t, 2, len(f.Args))
assertNum(t, 2, len(f.Returns))
arg := f.Args[0]
assertName(t, "a", arg.Name)
assertName(t, "string", arg.ArgType)
arg = f.Args[1]
assertName(t, "b", arg.Name)
assertName(t, "string", arg.ArgType)
arg = f.Returns[0]
assertName(t, "s", arg.Name)
assertName(t, "string", arg.ArgType)
arg = f.Returns[1]
assertName(t, "err", arg.Name)
assertName(t, "error", arg.ArgType)
}
func assertName(t *testing.T, expected, actual string) {
if expected != actual {
fatalOut(t, fmt.Sprintf("expected name to be `%s`, got: %s", expected, actual))
}
}
func assertNum(t *testing.T, expected, actual int) {
if expected != actual {
fatalOut(t, fmt.Sprintf("expected number to be %d, got: %d", expected, actual))
}
}
func fatalOut(t *testing.T, msg string) {
_, file, ln, _ := runtime.Caller(2)
t.Fatalf("%s:%d: %s", filepath.Base(file), ln, msg)
}

View File

@@ -0,0 +1,97 @@
package main
import (
"strings"
"text/template"
)
func printArgs(args []arg) string {
var argStr []string
for _, arg := range args {
argStr = append(argStr, arg.String())
}
return strings.Join(argStr, ", ")
}
func marshalType(t string) string {
switch t {
case "error":
// convert error types to plain strings to ensure the values are encoded/decoded properly
return "string"
default:
return t
}
}
func isErr(t string) bool {
switch t {
case "error":
return true
default:
return false
}
}
// Need to use this helper due to issues with go-vet
func buildTag(s string) string {
return "+build " + s
}
var templFuncs = template.FuncMap{
"printArgs": printArgs,
"marshalType": marshalType,
"isErr": isErr,
"lower": strings.ToLower,
"title": strings.Title,
"tag": buildTag,
}
var generatedTempl = template.Must(template.New("rpc_cient").Funcs(templFuncs).Parse(`
// generated code - DO NOT EDIT
{{ range $k, $v := .BuildTags }}
// {{ tag $k }} {{ end }}
package {{ .Name }}
import "errors"
type client interface{
Call(string, interface{}, interface{}) error
}
type {{ .InterfaceType }}Proxy struct {
client
}
{{ range .Functions }}
type {{ $.InterfaceType }}Proxy{{ .Name }}Request struct{
{{ range .Args }}
{{ title .Name }} {{ .ArgType }} {{ end }}
}
type {{ $.InterfaceType }}Proxy{{ .Name }}Response struct{
{{ range .Returns }}
{{ title .Name }} {{ marshalType .ArgType }} {{ end }}
}
func (pp *{{ $.InterfaceType }}Proxy) {{ .Name }}({{ printArgs .Args }}) ({{ printArgs .Returns }}) {
var(
req {{ $.InterfaceType }}Proxy{{ .Name }}Request
ret {{ $.InterfaceType }}Proxy{{ .Name }}Response
)
{{ range .Args }}
req.{{ title .Name }} = {{ lower .Name }} {{ end }}
if err = pp.Call("{{ $.RPCName }}.{{ .Name }}", req, &ret); err != nil {
return
}
{{ range $r := .Returns }}
{{ if isErr .ArgType }}
if ret.{{ title .Name }} != "" {
{{ lower .Name }} = errors.New(ret.{{ title .Name }})
} {{ end }}
{{ if isErr .ArgType | not }} {{ lower .Name }} = ret.{{ title .Name }} {{ end }} {{ end }}
return
}
{{ end }}
`))

View File

@@ -0,0 +1,222 @@
// Package plugins provides structures and helper functions to manage Docker
// plugins.
//
// Docker discovers plugins by looking for them in the plugin directory whenever
// a user or container tries to use one by name. UNIX domain socket files must
// be located under /run/docker/plugins, whereas spec files can be located
// either under /etc/docker/plugins or /usr/lib/docker/plugins. This is handled
// by the Registry interface, which lets you list all plugins or get a plugin by
// its name if it exists.
//
// The plugins need to implement an HTTP server and bind this to the UNIX socket
// or the address specified in the spec files.
// A handshake is send at /Plugin.Activate, and plugins are expected to return
// a Manifest with a list of of Docker subsystems which this plugin implements.
//
// In order to use a plugins, you can use the ``Get`` with the name of the
// plugin and the subsystem it implements.
//
// plugin, err := plugins.Get("example", "VolumeDriver")
// if err != nil {
// return fmt.Errorf("Error looking up volume plugin example: %v", err)
// }
package plugins
import (
"errors"
"sync"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/go-connections/tlsconfig"
)
var (
// ErrNotImplements is returned if the plugin does not implement the requested driver.
ErrNotImplements = errors.New("Plugin does not implement the requested driver")
)
type plugins struct {
sync.Mutex
plugins map[string]*Plugin
}
var (
storage = plugins{plugins: make(map[string]*Plugin)}
extpointHandlers = make(map[string]func(string, *Client))
)
// Manifest lists what a plugin implements.
type Manifest struct {
// List of subsystem the plugin implements.
Implements []string
}
// Plugin is the definition of a docker plugin.
type Plugin struct {
// Name of the plugin
Name string `json:"-"`
// Address of the plugin
Addr string
// TLS configuration of the plugin
TLSConfig tlsconfig.Options
// Client attached to the plugin
Client *Client `json:"-"`
// Manifest of the plugin (see above)
Manifest *Manifest `json:"-"`
activatErr error
activateOnce sync.Once
}
func newLocalPlugin(name, addr string) *Plugin {
return &Plugin{
Name: name,
Addr: addr,
TLSConfig: tlsconfig.Options{InsecureSkipVerify: true},
}
}
func (p *Plugin) activate() error {
p.activateOnce.Do(func() {
p.activatErr = p.activateWithLock()
})
return p.activatErr
}
func (p *Plugin) activateWithLock() error {
c, err := NewClient(p.Addr, p.TLSConfig)
if err != nil {
return err
}
p.Client = c
m := new(Manifest)
if err = p.Client.Call("Plugin.Activate", nil, m); err != nil {
return err
}
p.Manifest = m
for _, iface := range m.Implements {
handler, handled := extpointHandlers[iface]
if !handled {
continue
}
handler(p.Name, p.Client)
}
return nil
}
func (p *Plugin) implements(kind string) bool {
for _, driver := range p.Manifest.Implements {
if driver == kind {
return true
}
}
return false
}
func load(name string) (*Plugin, error) {
return loadWithRetry(name, true)
}
func loadWithRetry(name string, retry bool) (*Plugin, error) {
registry := newLocalRegistry()
start := time.Now()
var retries int
for {
pl, err := registry.Plugin(name)
if err != nil {
if !retry {
return nil, err
}
timeOff := backoff(retries)
if abort(start, timeOff) {
return nil, err
}
retries++
logrus.Warnf("Unable to locate plugin: %s, retrying in %v", name, timeOff)
time.Sleep(timeOff)
continue
}
storage.Lock()
storage.plugins[name] = pl
storage.Unlock()
err = pl.activate()
if err != nil {
storage.Lock()
delete(storage.plugins, name)
storage.Unlock()
}
return pl, err
}
}
func get(name string) (*Plugin, error) {
storage.Lock()
pl, ok := storage.plugins[name]
storage.Unlock()
if ok {
return pl, pl.activate()
}
return load(name)
}
// Get returns the plugin given the specified name and requested implementation.
func Get(name, imp string) (*Plugin, error) {
pl, err := get(name)
if err != nil {
return nil, err
}
if pl.implements(imp) {
logrus.Debugf("%s implements: %s", name, imp)
return pl, nil
}
return nil, ErrNotImplements
}
// Handle adds the specified function to the extpointHandlers.
func Handle(iface string, fn func(string, *Client)) {
extpointHandlers[iface] = fn
}
// GetAll returns all the plugins for the specified implementation
func GetAll(imp string) ([]*Plugin, error) {
pluginNames, err := Scan()
if err != nil {
return nil, err
}
type plLoad struct {
pl *Plugin
err error
}
chPl := make(chan plLoad, len(pluginNames))
for _, name := range pluginNames {
go func(name string) {
pl, err := loadWithRetry(name, false)
chPl <- plLoad{pl, err}
}(name)
}
var out []*Plugin
for i := 0; i < len(pluginNames); i++ {
pl := <-chPl
if pl.err != nil {
logrus.Error(err)
continue
}
if pl.pl.implements(imp) {
out = append(out, pl.pl)
}
}
return out, nil
}