Show volume options for `docker volume inspect`

This fix tries to address the issue raised in 25545 where
volume options at the creation time is not showed up
in `docker volume inspect`.

This fix adds the field `Options` in `Volume` type and
persist the options in volume db so that `volume inspect`
could display the options.

This fix adds a couple of test cases to cover the changes.

This fix fixes 25545.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
This commit is contained in:
Yong Tang 2016-09-17 12:32:31 -07:00
parent 1b677816f5
commit 9ce8aac55e
8 changed files with 85 additions and 32 deletions

View File

@ -447,6 +447,7 @@ type Volume struct {
Status map[string]interface{} `json:",omitempty"` // Status provides low-level status information about the volume
Labels map[string]string // Labels is metadata specific to the volume
Scope string // Scope describes the level at which the volume exists (e.g. `global` for cluster-wide or `local` for machine level)
Options map[string]string // Options holds the driver specific options to use for when creating the volume.
UsageData *VolumeUsageData `json:",omitempty"`
}

View File

@ -608,7 +608,7 @@ func (daemon *Daemon) filterVolumes(vols []volume.Volume, filter filters.Args) (
}
}
if filter.Include("label") {
v, ok := vol.(volume.LabeledVolume)
v, ok := vol.(volume.DetailedVolume)
if !ok {
continue
}

View File

@ -32,13 +32,12 @@ func volumeToAPIType(v volume.Volume) *types.Volume {
Name: v.Name(),
Driver: v.DriverName(),
}
if v, ok := v.(volume.LabeledVolume); ok {
if v, ok := v.(volume.DetailedVolume); ok {
tv.Labels = v.Labels()
}
if v, ok := v.(volume.ScopedVolume); ok {
tv.Options = v.Options()
tv.Scope = v.Scope()
}
return tv
}

View File

@ -132,6 +132,7 @@ This section lists each version from latest to oldest. Each listing includes a
* `POST /services/create` and `POST /services/(id or name)/update` now accept `Monitor` and `MaxFailureRatio` parameters, which control the response to failures during service updates.
* `GET /networks/(name)` now returns `Created`.
* `POST /containers/(id or name)/exec` now accepts an `Env` field, which holds a list of environment variables to be set in the context of the command execution.
* `GET /volumes`, `GET /volumes/(name)`, and `POST /volumes/create` now return the `Options` field which holds the driver specific options to use for when creating the volume.
### v1.24 API changes

View File

@ -2661,6 +2661,7 @@ Return docker data usage information
"Mountpoint": "",
"Labels": null,
"Scope": "",
"Options": null
"UsageData": {
"Size": 0,
"RefCount": 0
@ -3328,7 +3329,12 @@ Return low-level information about the `exec` command `id`.
"com.example.some-label": "some-value",
"com.example.some-other-label": "some-other-value"
},
"Scope": "local"
"Scope": "local",
"Options": {
"device": "tmpfs",
"o": "size=100m,uid=1000",
"type": "tmpfs"
}
}
],
"Warnings": []
@ -3382,7 +3388,8 @@ Create a volume
"com.example.some-label": "some-value",
"com.example.some-other-label": "some-other-value"
},
"Scope": "local"
"Scope": "local",
"Options": null
}
**Status codes**:
@ -3429,7 +3436,11 @@ Return low-level information on the volume `name`
"com.example.some-label": "some-value",
"com.example.some-other-label": "some-other-value"
},
"Scope": "local"
"Scope": "local",
"Options": {
"some-key": "some-value",
"some-other-key": "some-other-value"
},
}
**Status codes**:
@ -3454,6 +3465,7 @@ response.
- **Labels** - Labels set on the volume, specified as a map: `{"key":"value","key2":"value2"}`.
- **Scope** - Scope describes the level at which the volume exists, can be one of
`global` for cluster-wide or `local` for machine level. The default is `local`.
- **Options** - Options holds the driver specific options to use for when creating the volume.
### Remove a volume

View File

@ -1,6 +1,7 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
@ -418,3 +419,24 @@ func (s *DockerSuite) TestVolumeCLIRmForce(c *check.C) {
out, _ = dockerCmd(c, "volume", "ls")
c.Assert(out, checker.Contains, name)
}
func (s *DockerSuite) TestVolumeCliInspectWithVolumeOpts(c *check.C) {
testRequires(c, DaemonIsLinux)
// Without options
name := "test1"
dockerCmd(c, "volume", "create", "-d", "local", name)
out, _ := dockerCmd(c, "volume", "inspect", "--format={{ .Options }}", name)
c.Assert(strings.TrimSpace(out), checker.Contains, "map[]")
// With options
name = "test2"
k1, v1 := "type", "tmpfs"
k2, v2 := "device", "tmpfs"
k3, v3 := "o", "size=1m,uid=1000"
dockerCmd(c, "volume", "create", "-d", "local", name, "--opt", fmt.Sprintf("%s=%s", k1, v1), "--opt", fmt.Sprintf("%s=%s", k2, v2), "--opt", fmt.Sprintf("%s=%s", k3, v3))
out, _ = dockerCmd(c, "volume", "inspect", "--format={{ .Options }}", name)
c.Assert(strings.TrimSpace(out), checker.Contains, fmt.Sprintf("%s:%s", k1, v1))
c.Assert(strings.TrimSpace(out), checker.Contains, fmt.Sprintf("%s:%s", k2, v2))
c.Assert(strings.TrimSpace(out), checker.Contains, fmt.Sprintf("%s:%s", k3, v3))
}

View File

@ -23,14 +23,24 @@ const (
)
type volumeMetadata struct {
Name string
Labels map[string]string
Name string
Labels map[string]string
Options map[string]string
}
type volumeWrapper struct {
volume.Volume
labels map[string]string
scope string
labels map[string]string
scope string
options map[string]string
}
func (v volumeWrapper) Options() map[string]string {
options := map[string]string{}
for key, value := range v.options {
options[key] = value
}
return options
}
func (v volumeWrapper) Labels() map[string]string {
@ -54,10 +64,11 @@ func (v volumeWrapper) CachedPath() string {
// reference counting of volumes in the system.
func New(rootPath string) (*VolumeStore, error) {
vs := &VolumeStore{
locks: &locker.Locker{},
names: make(map[string]volume.Volume),
refs: make(map[string][]string),
labels: make(map[string]map[string]string),
locks: &locker.Locker{},
names: make(map[string]volume.Volume),
refs: make(map[string][]string),
labels: make(map[string]map[string]string),
options: make(map[string]map[string]string),
}
if rootPath != "" {
@ -113,6 +124,7 @@ func (s *VolumeStore) Purge(name string) {
delete(s.names, name)
delete(s.refs, name)
delete(s.labels, name)
delete(s.options, name)
s.globalLock.Unlock()
}
@ -127,7 +139,9 @@ type VolumeStore struct {
refs map[string][]string
// labels stores volume labels for each volume
labels map[string]map[string]string
db *bolt.DB
// options stores volume options for each volume
options map[string]map[string]string
db *bolt.DB
}
// List proxies to all registered volume drivers to get the full list of volumes
@ -186,7 +200,7 @@ func (s *VolumeStore) list() ([]volume.Volume, []string, error) {
return
}
for i, v := range vs {
vs[i] = volumeWrapper{v, s.labels[v.Name()], d.Scope()}
vs[i] = volumeWrapper{v, s.labels[v.Name()], d.Scope(), s.options[v.Name()]}
}
chVols <- vols{vols: vs}
@ -283,12 +297,14 @@ func (s *VolumeStore) create(name, driverName string, opts, labels map[string]st
}
s.globalLock.Lock()
s.labels[name] = labels
s.options[name] = opts
s.globalLock.Unlock()
if s.db != nil {
metadata := &volumeMetadata{
Name: name,
Labels: labels,
Name: name,
Labels: labels,
Options: opts,
}
volData, err := json.Marshal(metadata)
@ -305,7 +321,7 @@ func (s *VolumeStore) create(name, driverName string, opts, labels map[string]st
}
}
return volumeWrapper{v, labels, vd.Scope()}, nil
return volumeWrapper{v, labels, vd.Scope(), opts}, nil
}
// GetWithRef gets a volume with the given name from the passed in driver and stores the ref
@ -328,7 +344,7 @@ func (s *VolumeStore) GetWithRef(name, driverName, ref string) (volume.Volume, e
s.setNamed(v, ref)
return volumeWrapper{v, s.labels[name], vd.Scope()}, nil
return volumeWrapper{v, s.labels[name], vd.Scope(), s.options[name]}, nil
}
// Get looks if a volume with the given name exists and returns it if so
@ -350,6 +366,7 @@ func (s *VolumeStore) Get(name string) (volume.Volume, error) {
// it is expected that callers of this function hold any necessary locks
func (s *VolumeStore) getVolume(name string) (volume.Volume, error) {
labels := map[string]string{}
options := map[string]string{}
if s.db != nil {
// get meta
@ -368,6 +385,7 @@ func (s *VolumeStore) getVolume(name string) (volume.Volume, error) {
return err
}
labels = meta.Labels
options = meta.Options
return nil
}); err != nil {
@ -388,7 +406,7 @@ func (s *VolumeStore) getVolume(name string) (volume.Volume, error) {
if err != nil {
return nil, err
}
return volumeWrapper{vol, labels, vd.Scope()}, nil
return volumeWrapper{vol, labels, vd.Scope(), options}, nil
}
logrus.Debugf("Probing all drivers for volume with name: %s", name)
@ -403,7 +421,7 @@ func (s *VolumeStore) getVolume(name string) (volume.Volume, error) {
continue
}
return volumeWrapper{v, labels, d.Scope()}, nil
return volumeWrapper{v, labels, d.Scope(), options}, nil
}
return nil, errNoSuchVolume
}
@ -478,7 +496,11 @@ func (s *VolumeStore) FilterByDriver(name string) ([]volume.Volume, error) {
return nil, &OpErr{Err: err, Name: name, Op: "list"}
}
for i, v := range ls {
ls[i] = volumeWrapper{v, s.labels[v.Name()], vd.Scope()}
options := map[string]string{}
for key, value := range s.options[v.Name()] {
options[key] = value
}
ls[i] = volumeWrapper{v, s.labels[v.Name()], vd.Scope(), options}
}
return ls, nil
}

View File

@ -68,14 +68,10 @@ type Volume interface {
Status() map[string]interface{}
}
// LabeledVolume wraps a Volume with user-defined labels
type LabeledVolume interface {
// DetailedVolume wraps a Volume with user-defined labels, options, and cluster scope (e.g., `local` or `global`)
type DetailedVolume interface {
Labels() map[string]string
Volume
}
// ScopedVolume wraps a volume with a cluster scope (e.g., `local` or `global`)
type ScopedVolume interface {
Options() map[string]string
Scope() string
Volume
}