Skip to content

Commit

Permalink
Add support for volume scopes
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
cpuguy83 committed Jun 5, 2016
1 parent 79ff6ea commit 2f40b1b
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 23 deletions.
4 changes: 3 additions & 1 deletion daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
8 changes: 5 additions & 3 deletions daemon/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
27 changes: 27 additions & 0 deletions docs/extend/plugins_volume.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 changes: 29 additions & 0 deletions integration-cli/docker_cli_external_volume_driver_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -35,6 +36,7 @@ type eventCounter struct {
paths int
lists int
gets int
caps int
}

type DockerExternalVolumeSuite struct {
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion pkg/plugins/pluginrpc-gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down
48 changes: 44 additions & 4 deletions volume/drivers/adapter.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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{
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions volume/drivers/extpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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) {
Expand Down
32 changes: 31 additions & 1 deletion volume/drivers/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

package volumedrivers

import "errors"
import (
"errors"
"github.com/docker/docker/volume"
)

type client interface {
Call(string, interface{}, interface{}) error
Expand Down Expand Up @@ -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 changes: 10 additions & 0 deletions volume/drivers/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 changes: 5 additions & 0 deletions volume/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down
Loading

0 comments on commit 2f40b1b

Please sign in to comment.