From 79ff6eaf21dfebad0f8131a1ede235249cd6638f Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Sat, 9 Apr 2016 17:42:24 -0400 Subject: [PATCH 1/2] Enhance pluginrpc-gen parser Now handles `package.Type` and `*package.Type` Fixes parsing issues with slice and map types. Signed-off-by: Brian Goff --- pkg/plugins/pluginrpc-gen/README.md | 10 -- pkg/plugins/pluginrpc-gen/fixtures/foo.go | 48 +++++++ .../fixtures/otherfixture/spaceship.go | 4 + pkg/plugins/pluginrpc-gen/main.go | 2 +- pkg/plugins/pluginrpc-gen/parser.go | 132 +++++++++++++++--- pkg/plugins/pluginrpc-gen/parser_test.go | 56 +++++++- pkg/plugins/pluginrpc-gen/template.go | 16 ++- volume/drivers/extpoint.go | 7 +- volume/drivers/proxy.go | 8 +- 9 files changed, 245 insertions(+), 38 deletions(-) create mode 100644 pkg/plugins/pluginrpc-gen/fixtures/otherfixture/spaceship.go diff --git a/pkg/plugins/pluginrpc-gen/README.md b/pkg/plugins/pluginrpc-gen/README.md index 98720b2127..0418a3e00a 100644 --- a/pkg/plugins/pluginrpc-gen/README.md +++ b/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. diff --git a/pkg/plugins/pluginrpc-gen/fixtures/foo.go b/pkg/plugins/pluginrpc-gen/fixtures/foo.go index fcb2b6231d..5695dcc2d4 100644 --- a/pkg/plugins/pluginrpc-gen/fixtures/foo.go +++ b/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) +} diff --git a/pkg/plugins/pluginrpc-gen/fixtures/otherfixture/spaceship.go b/pkg/plugins/pluginrpc-gen/fixtures/otherfixture/spaceship.go new file mode 100644 index 0000000000..1937d1786c --- /dev/null +++ b/pkg/plugins/pluginrpc-gen/fixtures/otherfixture/spaceship.go @@ -0,0 +1,4 @@ +package otherfixture + +// Spaceship is a fixture for tests +type Spaceship struct{} diff --git a/pkg/plugins/pluginrpc-gen/main.go b/pkg/plugins/pluginrpc-gen/main.go index 772984ce3a..402044c87f 100644 --- a/pkg/plugins/pluginrpc-gen/main.go +++ b/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 formating generated source:\n"+buf.String(), err) errorOut("error writing file", ioutil.WriteFile(*outputFile, src, 0644)) } diff --git a/pkg/plugins/pluginrpc-gen/parser.go b/pkg/plugins/pluginrpc-gen/parser.go index 3adeb4905c..6c547e18cf 100644 --- a/pkg/plugins/pluginrpc-gen/parser.go +++ b/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 +} diff --git a/pkg/plugins/pluginrpc-gen/parser_test.go b/pkg/plugins/pluginrpc-gen/parser_test.go index 5a7579cfa8..a1b1ac9567 100644 --- a/pkg/plugins/pluginrpc-gen/parser_test.go +++ b/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)) diff --git a/pkg/plugins/pluginrpc-gen/template.go b/pkg/plugins/pluginrpc-gen/template.go index d3dc494dd4..50ed9293c1 100644 --- a/pkg/plugins/pluginrpc-gen/template.go +++ b/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 diff --git a/volume/drivers/extpoint.go b/volume/drivers/extpoint.go index c792f5e31d..e805d86b97 100644 --- a/volume/drivers/extpoint.go +++ b/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,7 +39,7 @@ 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) } diff --git a/volume/drivers/proxy.go b/volume/drivers/proxy.go index f55e0dbeaf..df08d88111 100644 --- a/volume/drivers/proxy.go +++ b/volume/drivers/proxy.go @@ -14,14 +14,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 +158,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 From 2f40b1b281a3be8f34d82a5170988ee46ea1f442 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Mon, 11 Apr 2016 11:17:52 -0400 Subject: [PATCH 2/2] Add support for volume scopes This is similar to network scopes where a volume can either be `local` or `global`. A `global` volume is one that exists across the entire cluster where as a `local` volume exists on a single engine. Signed-off-by: Brian Goff --- daemon/daemon.go | 4 +- daemon/volumes.go | 8 ++-- docs/extend/plugins_volume.md | 27 +++++++++++ ...er_cli_external_volume_driver_unix_test.go | 29 +++++++++++ pkg/plugins/pluginrpc-gen/main.go | 2 +- volume/drivers/adapter.go | 48 +++++++++++++++++-- volume/drivers/extpoint.go | 19 ++++++++ volume/drivers/proxy.go | 32 ++++++++++++- volume/drivers/proxy_test.go | 10 ++++ volume/local/local.go | 5 ++ volume/store/store.go | 43 ++++++++++++----- volume/testutils/testutils.go | 5 ++ volume/volume.go | 33 ++++++++++++- 13 files changed, 242 insertions(+), 23 deletions(-) diff --git a/daemon/daemon.go b/daemon/daemon.go index 7d095930e1..baf1617ffc 100644 --- a/daemon/daemon.go +++ b/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) } diff --git a/daemon/volumes.go b/daemon/volumes.go index 48359b614e..fd70ce4a92 100644 --- a/daemon/volumes.go +++ b/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 } diff --git a/docs/extend/plugins_volume.md b/docs/extend/plugins_volume.md index 5c2ebb49bc..a70a2c3ee2 100644 --- a/docs/extend/plugins_volume.md +++ b/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. diff --git a/integration-cli/docker_cli_external_volume_driver_unix_test.go b/integration-cli/docker_cli_external_volume_driver_unix_test.go index 134069135e..726534597d 100644 --- a/integration-cli/docker_cli_external_volume_driver_unix_test.go +++ b/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) + } +} diff --git a/pkg/plugins/pluginrpc-gen/main.go b/pkg/plugins/pluginrpc-gen/main.go index 402044c87f..e77a7d45ff 100644 --- a/pkg/plugins/pluginrpc-gen/main.go +++ b/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:\n"+buf.String(), err) + errorOut("error formatting generated source:\n"+buf.String(), err) errorOut("error writing file", ioutil.WriteFile(*outputFile, src, 0644)) } diff --git a/volume/drivers/adapter.go b/volume/drivers/adapter.go index 1593d01669..5193f1acbc 100644 --- a/volume/drivers/adapter.go +++ b/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 diff --git a/volume/drivers/extpoint.go b/volume/drivers/extpoint.go index e805d86b97..ebcf26cc34 100644 --- a/volume/drivers/extpoint.go +++ b/volume/drivers/extpoint.go @@ -42,6 +42,8 @@ type volumeDriver interface { 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 { @@ -64,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 } @@ -107,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) { diff --git a/volume/drivers/proxy.go b/volume/drivers/proxy.go index df08d88111..431cb2ec01 100644 --- a/volume/drivers/proxy.go +++ b/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 @@ -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 +} diff --git a/volume/drivers/proxy_test.go b/volume/drivers/proxy_test.go index 62a217baa9..455f8d3fc9 100644 --- a/volume/drivers/proxy_test.go +++ b/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) + } } diff --git a/volume/local/local.go b/volume/local/local.go index b0046b6f55..caac4a411c 100644 --- a/volume/local/local.go +++ b/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)} diff --git a/volume/store/store.go b/volume/store/store.go index b2ac9a8ce8..962c735b36 100644 --- a/volume/store/store.go +++ b/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 } diff --git a/volume/testutils/testutils.go b/volume/testutils/testutils.go index 91cdcded00..30850d00bd 100644 --- a/volume/testutils/testutils.go +++ b/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" +} diff --git a/volume/volume.go b/volume/volume.go index 8999e11484..1b57d85087 100644 --- a/volume/volume.go +++ b/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.