Ver código fonte

Merge pull request #22077 from cpuguy83/remote_volplugin_caps

Add support for volume scopes
Vincent Demeester 9 anos atrás
pai
commit
28dde09cdf

+ 3 - 1
daemon/daemon.go

@@ -745,7 +745,9 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore,
 		return nil, err
 	}
 
-	volumedrivers.Register(volumesDriver, volumesDriver.Name())
+	if !volumedrivers.Register(volumesDriver, volumesDriver.Name()) {
+		return nil, fmt.Errorf("local volume driver could not be registered")
+	}
 	return store.New(config.Root)
 }
 

+ 5 - 3
daemon/volumes.go

@@ -27,11 +27,13 @@ func volumeToAPIType(v volume.Volume) *types.Volume {
 		Name:   v.Name(),
 		Driver: v.DriverName(),
 	}
-	if v, ok := v.(interface {
-		Labels() map[string]string
-	}); ok {
+	if v, ok := v.(volume.LabeledVolume); ok {
 		tv.Labels = v.Labels()
 	}
+
+	if v, ok := v.(volume.ScopedVolume); ok {
+		tv.Scope = v.Scope()
+	}
 	return tv
 }
 

+ 27 - 0
docs/extend/plugins_volume.md

@@ -20,6 +20,7 @@ documentation](plugins.md) for more information.
 ### 1.12.0
 
 - Add `Status` field to `VolumeDriver.Get` response ([#21006](https://github.com/docker/docker/pull/21006#))
+- Add `VolumeDriver.Capabilities` to get capabilities of the volume driver([#22077](https://github.com/docker/docker/pull/22077))
 
 ### 1.10.0
 
@@ -236,3 +237,29 @@ Get the list of volumes registered with the plugin.
 ```
 
 Respond with a string error if an error occurred.
+
+### /VolumeDriver.Capabilities
+
+**Request**:
+```json
+{}
+```
+
+Get the list of capabilities the driver supports.
+The driver is not required to implement this endpoint, however in such cases
+the default values will be taken.
+
+**Response**:
+```json
+{
+  "Capabilities": {
+    "Scope": "global"
+  }
+}
+```
+
+Supported scopes are `global` and `local`. Any other value in `Scope` will be
+ignored and assumed to be `local`. Scope allows cluster managers to handle the
+volume differently, for instance with a scope of `global`, the cluster manager
+knows it only needs to create the volume once instead of on every engine. More
+capabilities may be added in the future.

+ 29 - 0
integration-cli/docker_cli_external_volume_driver_unix_test.go

@@ -16,6 +16,7 @@ import (
 	"time"
 
 	"github.com/docker/docker/pkg/integration/checker"
+	"github.com/docker/docker/volume"
 	"github.com/docker/engine-api/types"
 	"github.com/go-check/check"
 )
@@ -35,6 +36,7 @@ type eventCounter struct {
 	paths       int
 	lists       int
 	gets        int
+	caps        int
 }
 
 type DockerExternalVolumeSuite struct {
@@ -225,6 +227,18 @@ func (s *DockerExternalVolumeSuite) SetUpSuite(c *check.C) {
 		send(w, nil)
 	})
 
+	mux.HandleFunc("/VolumeDriver.Capabilities", func(w http.ResponseWriter, r *http.Request) {
+		s.ec.caps++
+
+		_, err := read(r.Body)
+		if err != nil {
+			send(w, err)
+			return
+		}
+
+		send(w, `{"Capabilities": { "Scope": "global" }}`)
+	})
+
 	err := os.MkdirAll("/etc/docker/plugins", 0755)
 	c.Assert(err, checker.IsNil)
 
@@ -491,3 +505,18 @@ func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverMountID(c *check.C)
 	c.Assert(err, checker.IsNil, check.Commentf(out))
 	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
 }
+
+// Check that VolumeDriver.Capabilities gets called, and only called once
+func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverCapabilities(c *check.C) {
+	c.Assert(s.d.Start(), checker.IsNil)
+	c.Assert(s.ec.caps, checker.Equals, 0)
+
+	for i := 0; i < 3; i++ {
+		out, err := s.d.Cmd("volume", "create", "-d", "test-external-volume-driver", "--name", fmt.Sprintf("test%d", i))
+		c.Assert(err, checker.IsNil, check.Commentf(out))
+		c.Assert(s.ec.caps, checker.Equals, 1)
+		out, err = s.d.Cmd("volume", "inspect", "--format={{.Scope}}", fmt.Sprintf("test%d", i))
+		c.Assert(err, checker.IsNil)
+		c.Assert(strings.TrimSpace(out), checker.Equals, volume.GlobalScope)
+	}
+}

+ 0 - 10
pkg/plugins/pluginrpc-gen/README.md

@@ -43,16 +43,6 @@ 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.  

+ 48 - 0
pkg/plugins/pluginrpc-gen/fixtures/foo.go

@@ -1,5 +1,17 @@
 package foo
 
+import (
+	"fmt"
+
+	aliasedio "io"
+
+	"github.com/docker/docker/pkg/plugins/pluginrpc-gen/fixtures/otherfixture"
+)
+
+var (
+	errFakeImport = fmt.Errorf("just to import fmt for imports tests")
+)
+
 type wobble struct {
 	Some      string
 	Val       string
@@ -22,6 +34,7 @@ type Fooer3 interface {
 	Qux(a, b string) (val string, err error)
 	Wobble() (w *wobble)
 	Wiggle() (w wobble)
+	WiggleWobble(a []*wobble, b []wobble, c map[string]*wobble, d map[*wobble]wobble, e map[string][]wobble, f []*otherfixture.Spaceship) (g map[*wobble]wobble, h [][]*wobble, i otherfixture.Spaceship, j *otherfixture.Spaceship, k map[*otherfixture.Spaceship]otherfixture.Spaceship, l []otherfixture.Spaceship)
 }
 
 // Fooer4 is an interface used for tests.
@@ -39,3 +52,38 @@ type Fooer5 interface {
 	Foo()
 	Bar
 }
+
+// Fooer6 is an interface used for tests.
+type Fooer6 interface {
+	Foo(a otherfixture.Spaceship)
+}
+
+// Fooer7 is an interface used for tests.
+type Fooer7 interface {
+	Foo(a *otherfixture.Spaceship)
+}
+
+// Fooer8 is an interface used for tests.
+type Fooer8 interface {
+	Foo(a map[string]otherfixture.Spaceship)
+}
+
+// Fooer9 is an interface used for tests.
+type Fooer9 interface {
+	Foo(a map[string]*otherfixture.Spaceship)
+}
+
+// Fooer10 is an interface used for tests.
+type Fooer10 interface {
+	Foo(a []otherfixture.Spaceship)
+}
+
+// Fooer11 is an interface used for tests.
+type Fooer11 interface {
+	Foo(a []*otherfixture.Spaceship)
+}
+
+// Fooer12 is an interface used for tests.
+type Fooer12 interface {
+	Foo(a aliasedio.Reader)
+}

+ 4 - 0
pkg/plugins/pluginrpc-gen/fixtures/otherfixture/spaceship.go

@@ -0,0 +1,4 @@
+package otherfixture
+
+// Spaceship is a fixture for tests
+type Spaceship struct{}

+ 1 - 1
pkg/plugins/pluginrpc-gen/main.go

@@ -78,7 +78,7 @@ func main() {
 
 	errorOut("parser error", generatedTempl.Execute(&buf, analysis))
 	src, err := format.Source(buf.Bytes())
-	errorOut("error formating generated source", err)
+	errorOut("error formatting generated source:\n"+buf.String(), err)
 	errorOut("error writing file", ioutil.WriteFile(*outputFile, src, 0644))
 }
 

+ 116 - 16
pkg/plugins/pluginrpc-gen/parser.go

@@ -6,7 +6,9 @@ import (
 	"go/ast"
 	"go/parser"
 	"go/token"
+	"path"
 	"reflect"
+	"strings"
 )
 
 var errBadReturn = errors.New("found return arg with no name: all args must be named")
@@ -25,6 +27,7 @@ func (e errUnexpectedType) Error() string {
 type ParsedPkg struct {
 	Name      string
 	Functions []function
+	Imports   []importSpec
 }
 
 type function struct {
@@ -35,14 +38,29 @@ type function struct {
 }
 
 type arg struct {
-	Name    string
-	ArgType string
+	Name            string
+	ArgType         string
+	PackageSelector string
 }
 
 func (a *arg) String() string {
 	return a.Name + " " + a.ArgType
 }
 
+type importSpec struct {
+	Name string
+	Path string
+}
+
+func (s *importSpec) String() string {
+	var ss string
+	if len(s.Name) != 0 {
+		ss += s.Name
+	}
+	ss += s.Path
+	return ss
+}
+
 // Parse parses the given file for an interface definition with the given name.
 func Parse(filePath string, objName string) (*ParsedPkg, error) {
 	fs := token.NewFileSet()
@@ -73,6 +91,44 @@ func Parse(filePath string, objName string) (*ParsedPkg, error) {
 		return nil, err
 	}
 
+	// figure out what imports will be needed
+	imports := make(map[string]importSpec)
+	for _, f := range p.Functions {
+		args := append(f.Args, f.Returns...)
+		for _, arg := range args {
+			if len(arg.PackageSelector) == 0 {
+				continue
+			}
+
+			for _, i := range pkg.Imports {
+				if i.Name != nil {
+					if i.Name.Name != arg.PackageSelector {
+						continue
+					}
+					imports[i.Path.Value] = importSpec{Name: arg.PackageSelector, Path: i.Path.Value}
+					break
+				}
+
+				_, name := path.Split(i.Path.Value)
+				splitName := strings.Split(name, "-")
+				if len(splitName) > 1 {
+					name = splitName[len(splitName)-1]
+				}
+				// import paths have quotes already added in, so need to remove them for name comparison
+				name = strings.TrimPrefix(name, `"`)
+				name = strings.TrimSuffix(name, `"`)
+				if name == arg.PackageSelector {
+					imports[i.Path.Value] = importSpec{Path: i.Path.Value}
+					break
+				}
+			}
+		}
+	}
+
+	for _, spec := range imports {
+		p.Imports = append(p.Imports, spec)
+	}
+
 	return p, nil
 }
 
@@ -142,22 +198,66 @@ func parseArgs(fields []*ast.Field) ([]arg, error) {
 			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}
+			p, err := parseExpr(f.Type)
+			if err != nil {
+				return nil, err
 			}
-
-			args = append(args, arg{name.Name, typeName})
+			args = append(args, arg{name.Name, p.value, p.pkg})
 		}
 	}
 	return args, nil
 }
+
+type parsedExpr struct {
+	value string
+	pkg   string
+}
+
+func parseExpr(e ast.Expr) (parsedExpr, error) {
+	var parsed parsedExpr
+	switch i := e.(type) {
+	case *ast.Ident:
+		parsed.value += i.Name
+	case *ast.StarExpr:
+		p, err := parseExpr(i.X)
+		if err != nil {
+			return parsed, err
+		}
+		parsed.value += "*"
+		parsed.value += p.value
+		parsed.pkg = p.pkg
+	case *ast.SelectorExpr:
+		p, err := parseExpr(i.X)
+		if err != nil {
+			return parsed, err
+		}
+		parsed.pkg = p.value
+		parsed.value += p.value + "."
+		parsed.value += i.Sel.Name
+	case *ast.MapType:
+		parsed.value += "map["
+		p, err := parseExpr(i.Key)
+		if err != nil {
+			return parsed, err
+		}
+		parsed.value += p.value
+		parsed.value += "]"
+		p, err = parseExpr(i.Value)
+		if err != nil {
+			return parsed, err
+		}
+		parsed.value += p.value
+		parsed.pkg = p.pkg
+	case *ast.ArrayType:
+		parsed.value += "[]"
+		p, err := parseExpr(i.Elt)
+		if err != nil {
+			return parsed, err
+		}
+		parsed.value += p.value
+		parsed.pkg = p.pkg
+	default:
+		return parsed, errUnexpectedType{"*ast.Ident or *ast.StarExpr", i}
+	}
+	return parsed, nil
+}

+ 55 - 1
pkg/plugins/pluginrpc-gen/parser_test.go

@@ -47,7 +47,7 @@ func TestParseWithMultipleFuncs(t *testing.T) {
 	}
 
 	assertName(t, "foo", pkg.Name)
-	assertNum(t, 6, len(pkg.Functions))
+	assertNum(t, 7, len(pkg.Functions))
 
 	f := pkg.Functions[0]
 	assertName(t, "Foo", f.Name)
@@ -105,6 +105,35 @@ func TestParseWithMultipleFuncs(t *testing.T) {
 	arg = f.Returns[0]
 	assertName(t, "w", arg.Name)
 	assertName(t, "wobble", arg.ArgType)
+
+	f = pkg.Functions[6]
+	assertName(t, "WiggleWobble", f.Name)
+	assertNum(t, 6, len(f.Args))
+	assertNum(t, 6, len(f.Returns))
+	expectedArgs := [][]string{
+		{"a", "[]*wobble"},
+		{"b", "[]wobble"},
+		{"c", "map[string]*wobble"},
+		{"d", "map[*wobble]wobble"},
+		{"e", "map[string][]wobble"},
+		{"f", "[]*otherfixture.Spaceship"},
+	}
+	for i, arg := range f.Args {
+		assertName(t, expectedArgs[i][0], arg.Name)
+		assertName(t, expectedArgs[i][1], arg.ArgType)
+	}
+	expectedReturns := [][]string{
+		{"g", "map[*wobble]wobble"},
+		{"h", "[][]*wobble"},
+		{"i", "otherfixture.Spaceship"},
+		{"j", "*otherfixture.Spaceship"},
+		{"k", "map[*otherfixture.Spaceship]otherfixture.Spaceship"},
+		{"l", "[]otherfixture.Spaceship"},
+	}
+	for i, ret := range f.Returns {
+		assertName(t, expectedReturns[i][0], ret.Name)
+		assertName(t, expectedReturns[i][1], ret.ArgType)
+	}
 }
 
 func TestParseWithUnamedReturn(t *testing.T) {
@@ -150,6 +179,31 @@ func TestEmbeddedInterface(t *testing.T) {
 	assertName(t, "error", arg.ArgType)
 }
 
+func TestParsedImports(t *testing.T) {
+	cases := []string{"Fooer6", "Fooer7", "Fooer8", "Fooer9", "Fooer10", "Fooer11"}
+	for _, testCase := range cases {
+		pkg, err := Parse(testFixture, testCase)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assertNum(t, 1, len(pkg.Imports))
+		importPath := strings.Split(pkg.Imports[0].Path, "/")
+		assertName(t, "otherfixture\"", importPath[len(importPath)-1])
+		assertName(t, "", pkg.Imports[0].Name)
+	}
+}
+
+func TestAliasedImports(t *testing.T) {
+	pkg, err := Parse(testFixture, "Fooer12")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	assertNum(t, 1, len(pkg.Imports))
+	assertName(t, "aliasedio", pkg.Imports[0].Name)
+}
+
 func assertName(t *testing.T, expected, actual string) {
 	if expected != actual {
 		fatalOut(t, fmt.Sprintf("expected name to be `%s`, got: %s", expected, actual))

+ 15 - 1
pkg/plugins/pluginrpc-gen/template.go

@@ -13,6 +13,19 @@ func printArgs(args []arg) string {
 	return strings.Join(argStr, ", ")
 }
 
+func buildImports(specs []importSpec) string {
+	if len(specs) == 0 {
+		return `import "errors"`
+	}
+	imports := "import(\n"
+	imports += "\t\"errors\"\n"
+	for _, i := range specs {
+		imports += "\t" + i.String() + "\n"
+	}
+	imports += ")"
+	return imports
+}
+
 func marshalType(t string) string {
 	switch t {
 	case "error":
@@ -44,6 +57,7 @@ var templFuncs = template.FuncMap{
 	"lower":       strings.ToLower,
 	"title":       title,
 	"tag":         buildTag,
+	"imports":     buildImports,
 }
 
 func title(s string) string {
@@ -60,7 +74,7 @@ var generatedTempl = template.Must(template.New("rpc_cient").Funcs(templFuncs).P
 
 package {{ .Name }}
 
-import "errors"
+{{ imports .Imports }}
 
 type client interface{
 	Call(string, interface{}, interface{}) error

+ 44 - 4
volume/drivers/adapter.go

@@ -1,14 +1,22 @@
 package volumedrivers
 
 import (
-	"fmt"
+	"errors"
+	"strings"
 
+	"github.com/Sirupsen/logrus"
 	"github.com/docker/docker/volume"
 )
 
+var (
+	errInvalidScope = errors.New("invalid scope")
+	errNoSuchVolume = errors.New("no such volume")
+)
+
 type volumeDriverAdapter struct {
-	name  string
-	proxy *volumeDriverProxy
+	name         string
+	capabilities *volume.Capability
+	proxy        *volumeDriverProxy
 }
 
 func (a *volumeDriverAdapter) Name() string {
@@ -56,7 +64,7 @@ func (a *volumeDriverAdapter) Get(name string) (volume.Volume, error) {
 
 	// plugin may have returned no volume and no error
 	if v == nil {
-		return nil, fmt.Errorf("no such volume")
+		return nil, errNoSuchVolume
 	}
 
 	return &volumeAdapter{
@@ -68,6 +76,38 @@ func (a *volumeDriverAdapter) Get(name string) (volume.Volume, error) {
 	}, nil
 }
 
+func (a *volumeDriverAdapter) Scope() string {
+	cap := a.getCapabilities()
+	return cap.Scope
+}
+
+func (a *volumeDriverAdapter) getCapabilities() volume.Capability {
+	if a.capabilities != nil {
+		return *a.capabilities
+	}
+	cap, err := a.proxy.Capabilities()
+	if err != nil {
+		// `GetCapabilities` is a not a required endpoint.
+		// On error assume it's a local-only driver
+		logrus.Warnf("Volume driver %s returned an error while trying to query it's capabilities, using default capabilties: %v", a.name, err)
+		return volume.Capability{Scope: volume.LocalScope}
+	}
+
+	// don't spam the warn log below just because the plugin didn't provide a scope
+	if len(cap.Scope) == 0 {
+		cap.Scope = volume.LocalScope
+	}
+
+	cap.Scope = strings.ToLower(cap.Scope)
+	if cap.Scope != volume.LocalScope && cap.Scope != volume.GlobalScope {
+		logrus.Warnf("Volume driver %q returned an invalid scope: %q", a.Name(), cap.Scope)
+		cap.Scope = volume.LocalScope
+	}
+
+	a.capabilities = &cap
+	return cap
+}
+
 type volumeAdapter struct {
 	proxy      *volumeDriverProxy
 	name       string

+ 21 - 5
volume/drivers/extpoint.go

@@ -24,15 +24,12 @@ func NewVolumeDriver(name string, c client) volume.Driver {
 	return &volumeDriverAdapter{name: name, proxy: proxy}
 }
 
-type opts map[string]string
-type list []*proxyVolume
-
 // volumeDriver defines the available functions that volume plugins must implement.
 // This interface is only defined to generate the proxy objects.
 // It's not intended to be public or reused.
 type volumeDriver interface {
 	// Create a volume with the given name
-	Create(name string, opts opts) (err error)
+	Create(name string, opts map[string]string) (err error)
 	// Remove the volume with the given name
 	Remove(name string) (err error)
 	// Get the mountpoint of the given volume
@@ -42,9 +39,11 @@ type volumeDriver interface {
 	// Unmount the given volume
 	Unmount(name, id string) (err error)
 	// List lists all the volumes known to the driver
-	List() (volumes list, err error)
+	List() (volumes []*proxyVolume, err error)
 	// Get retrieves the volume with the requested name
 	Get(name string) (volume *proxyVolume, err error)
+	// Capabilities gets the list of capabilities of the driver
+	Capabilities() (capabilities volume.Capability, err error)
 }
 
 type driverExtpoint struct {
@@ -67,6 +66,11 @@ func Register(extension volume.Driver, name string) bool {
 	if exists {
 		return false
 	}
+
+	if err := validateDriver(extension); err != nil {
+		return false
+	}
+
 	drivers.extensions[name] = extension
 	return true
 }
@@ -110,10 +114,22 @@ func Lookup(name string) (volume.Driver, error) {
 	}
 
 	d := NewVolumeDriver(name, pl.Client)
+	if err := validateDriver(d); err != nil {
+		return nil, err
+	}
+
 	drivers.extensions[name] = d
 	return d, nil
 }
 
+func validateDriver(vd volume.Driver) error {
+	scope := vd.Scope()
+	if scope != volume.LocalScope && scope != volume.GlobalScope {
+		return fmt.Errorf("Driver %q provided an invalid capability scope: %s", vd.Name(), scope)
+	}
+	return nil
+}
+
 // GetDriver returns a volume driver by its name.
 // If the driver is empty, it looks for the local driver.
 func GetDriver(name string) (volume.Driver, error) {

+ 35 - 5
volume/drivers/proxy.go

@@ -2,7 +2,10 @@
 
 package volumedrivers
 
-import "errors"
+import (
+	"errors"
+	"github.com/docker/docker/volume"
+)
 
 type client interface {
 	Call(string, interface{}, interface{}) error
@@ -14,14 +17,14 @@ type volumeDriverProxy struct {
 
 type volumeDriverProxyCreateRequest struct {
 	Name string
-	Opts opts
+	Opts map[string]string
 }
 
 type volumeDriverProxyCreateResponse struct {
 	Err string
 }
 
-func (pp *volumeDriverProxy) Create(name string, opts opts) (err error) {
+func (pp *volumeDriverProxy) Create(name string, opts map[string]string) (err error) {
 	var (
 		req volumeDriverProxyCreateRequest
 		ret volumeDriverProxyCreateResponse
@@ -158,11 +161,11 @@ type volumeDriverProxyListRequest struct {
 }
 
 type volumeDriverProxyListResponse struct {
-	Volumes list
+	Volumes []*proxyVolume
 	Err     string
 }
 
-func (pp *volumeDriverProxy) List() (volumes list, err error) {
+func (pp *volumeDriverProxy) List() (volumes []*proxyVolume, err error) {
 	var (
 		req volumeDriverProxyListRequest
 		ret volumeDriverProxyListResponse
@@ -209,3 +212,30 @@ func (pp *volumeDriverProxy) Get(name string) (volume *proxyVolume, err error) {
 
 	return
 }
+
+type volumeDriverProxyCapabilitiesRequest struct {
+}
+
+type volumeDriverProxyCapabilitiesResponse struct {
+	Capabilities volume.Capability
+	Err          string
+}
+
+func (pp *volumeDriverProxy) Capabilities() (capabilities volume.Capability, err error) {
+	var (
+		req volumeDriverProxyCapabilitiesRequest
+		ret volumeDriverProxyCapabilitiesResponse
+	)
+
+	if err = pp.Call("VolumeDriver.Capabilities", req, &ret); err != nil {
+		return
+	}
+
+	capabilities = ret.Capabilities
+
+	if ret.Err != "" {
+		err = errors.New(ret.Err)
+	}
+
+	return
+}

+ 10 - 0
volume/drivers/proxy_test.go

@@ -52,6 +52,11 @@ func TestVolumeRequestError(t *testing.T) {
 		fmt.Fprintln(w, `{"Err": "Cannot get volume"}`)
 	})
 
+	mux.HandleFunc("/VolumeDriver.Capabilities", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
+		http.Error(w, "error", 500)
+	})
+
 	u, _ := url.Parse(server.URL)
 	client, err := plugins.NewClient("tcp://"+u.Host, tlsconfig.Options{InsecureSkipVerify: true})
 	if err != nil {
@@ -119,4 +124,9 @@ func TestVolumeRequestError(t *testing.T) {
 	if !strings.Contains(err.Error(), "Cannot get volume") {
 		t.Fatalf("Unexpected error: %v\n", err)
 	}
+
+	_, err = driver.Capabilities()
+	if err == nil {
+		t.Fatal(err)
+	}
 }

+ 5 - 0
volume/local/local.go

@@ -248,6 +248,11 @@ func (r *Root) Get(name string) (volume.Volume, error) {
 	return v, nil
 }
 
+// Scope returns the local volume scope
+func (r *Root) Scope() string {
+	return volume.LocalScope
+}
+
 func (r *Root) validateName(name string) error {
 	if !volumeNameRegex.MatchString(name) {
 		return validationError{fmt.Errorf("%q includes invalid characters for a local volume name, only %q are allowed", name, utils.RestrictedNameChars)}

+ 31 - 12
volume/store/store.go

@@ -25,15 +25,29 @@ type volumeMetadata struct {
 	Labels map[string]string
 }
 
-type volumeWithLabels struct {
+type volumeWrapper struct {
 	volume.Volume
 	labels map[string]string
+	scope  string
 }
 
-func (v volumeWithLabels) Labels() map[string]string {
+func (v volumeWrapper) Labels() map[string]string {
 	return v.labels
 }
 
+func (v volumeWrapper) Scope() string {
+	return v.scope
+}
+
+func (v volumeWrapper) CachedPath() string {
+	if vv, ok := v.Volume.(interface {
+		CachedPath() string
+	}); ok {
+		return vv.CachedPath()
+	}
+	return v.Volume.Path()
+}
+
 // New initializes a VolumeStore to keep
 // reference counting of volumes in the system.
 func New(rootPath string) (*VolumeStore, error) {
@@ -166,6 +180,10 @@ func (s *VolumeStore) list() ([]volume.Volume, []string, error) {
 				chVols <- vols{driverName: d.Name(), err: &OpErr{Err: err, Name: d.Name(), Op: "list"}}
 				return
 			}
+			for i, v := range vs {
+				vs[i] = volumeWrapper{v, s.labels[v.Name()], d.Scope()}
+			}
+
 			chVols <- vols{vols: vs}
 		}(vd)
 	}
@@ -291,7 +309,7 @@ func (s *VolumeStore) create(name, driverName string, opts, labels map[string]st
 		}
 	}
 
-	return volumeWithLabels{v, labels}, nil
+	return volumeWrapper{v, labels, vd.Scope()}, nil
 }
 
 // GetWithRef gets a volume with the given name from the passed in driver and stores the ref
@@ -313,10 +331,8 @@ func (s *VolumeStore) GetWithRef(name, driverName, ref string) (volume.Volume, e
 	}
 
 	s.setNamed(v, ref)
-	if labels, ok := s.labels[name]; ok {
-		return volumeWithLabels{v, labels}, nil
-	}
-	return v, nil
+
+	return volumeWrapper{v, s.labels[name], vd.Scope()}, nil
 }
 
 // Get looks if a volume with the given name exists and returns it if so
@@ -376,7 +392,7 @@ func (s *VolumeStore) getVolume(name string) (volume.Volume, error) {
 		if err != nil {
 			return nil, err
 		}
-		return volumeWithLabels{vol, labels}, nil
+		return volumeWrapper{vol, labels, vd.Scope()}, nil
 	}
 
 	logrus.Debugf("Probing all drivers for volume with name: %s", name)
@@ -391,7 +407,7 @@ func (s *VolumeStore) getVolume(name string) (volume.Volume, error) {
 			continue
 		}
 
-		return volumeWithLabels{v, labels}, nil
+		return volumeWrapper{v, labels, d.Scope()}, nil
 	}
 	return nil, errNoSuchVolume
 }
@@ -412,7 +428,7 @@ func (s *VolumeStore) Remove(v volume.Volume) error {
 	}
 
 	logrus.Debugf("Removing volume reference: driver %s, name %s", v.DriverName(), name)
-	vol := withoutLabels(v)
+	vol := unwrapVolume(v)
 	if err := vd.Remove(vol); err != nil {
 		return &OpErr{Err: err, Name: name, Op: "remove"}
 	}
@@ -465,6 +481,9 @@ func (s *VolumeStore) FilterByDriver(name string) ([]volume.Volume, error) {
 	if err != nil {
 		return nil, &OpErr{Err: err, Name: name, Op: "list"}
 	}
+	for i, v := range ls {
+		ls[i] = volumeWrapper{v, s.labels[v.Name()], vd.Scope()}
+	}
 	return ls, nil
 }
 
@@ -497,8 +516,8 @@ func (s *VolumeStore) filter(vols []volume.Volume, f filterFunc) []volume.Volume
 	return ls
 }
 
-func withoutLabels(v volume.Volume) volume.Volume {
-	if vol, ok := v.(volumeWithLabels); ok {
+func unwrapVolume(v volume.Volume) volume.Volume {
+	if vol, ok := v.(volumeWrapper); ok {
 		return vol.Volume
 	}
 

+ 5 - 0
volume/testutils/testutils.go

@@ -109,3 +109,8 @@ func (d *FakeDriver) Get(name string) (volume.Volume, error) {
 	}
 	return nil, fmt.Errorf("no such volume")
 }
+
+// Scope returns the local scope
+func (*FakeDriver) Scope() string {
+	return "local"
+}

+ 32 - 1
volume/volume.go

@@ -13,7 +13,14 @@ import (
 
 // DefaultDriverName is the driver name used for the driver
 // implemented in the local package.
-const DefaultDriverName string = "local"
+const DefaultDriverName = "local"
+
+// Scopes define if a volume has is cluster-wide (global) or local only.
+// Scopes are returned by the volume driver when it is queried for capabilities and then set on a volume
+const (
+	LocalScope  = "local"
+	GlobalScope = "global"
+)
 
 // Driver is for creating and removing volumes.
 type Driver interface {
@@ -27,6 +34,18 @@ type Driver interface {
 	List() ([]Volume, error)
 	// Get retrieves the volume with the requested name
 	Get(name string) (Volume, error)
+	// Scope returns the scope of the driver (e.g. `golbal` or `local`).
+	// Scope determines how the driver is handled at a cluster level
+	Scope() string
+}
+
+// Capability defines a set of capabilities that a driver is able to handle.
+type Capability struct {
+	// Scope is the scope of the driver, `global` or `local`
+	// A `global` scope indicates that the driver manages volumes across the cluster
+	// A `local` scope indicates that the driver only manages volumes resources local to the host
+	// Scope is declared by the driver
+	Scope string
 }
 
 // Volume is a place to store data. It is backed by a specific driver, and can be mounted.
@@ -46,6 +65,18 @@ type Volume interface {
 	Status() map[string]interface{}
 }
 
+// LabeledVolume wraps a Volume with user-defined labels
+type LabeledVolume interface {
+	Labels() map[string]string
+	Volume
+}
+
+// ScopedVolume wraps a volume with a cluster scope (e.g., `local` or `global`)
+type ScopedVolume interface {
+	Scope() string
+	Volume
+}
+
 // MountPoint is the intersection point between a volume and a container. It
 // specifies which volume is to be used and where inside a container it should
 // be mounted.