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 <cpuguy83@gmail.com>
This commit is contained in:
parent
79ff6eaf21
commit
2f40b1b281
13 changed files with 242 additions and 23 deletions
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Reference in a new issue